From dd6501c7c3f33b567c2a1deeece017ee7c23c7dd Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 27 Nov 2025 12:10:48 -0500 Subject: [PATCH 1/5] Start on a rework to keep from breaking conformal meshes while handling tagging --- assembly_mesh_plugin/plugin.py | 700 ++++++++++++++++++++++----------- tests/sample_assemblies.py | 32 ++ tests/test_meshes.py | 80 +++- 3 files changed, 569 insertions(+), 243 deletions(-) diff --git a/assembly_mesh_plugin/plugin.py b/assembly_mesh_plugin/plugin.py index 27a6613..391a97e 100644 --- a/assembly_mesh_plugin/plugin.py +++ b/assembly_mesh_plugin/plugin.py @@ -4,167 +4,390 @@ import cadquery as cq import gmsh +# The mesh volume and surface ids should line up with the order of solids and faces in the assembly +vol_id = 1 +surface_id = 1 -def get_tagged_gmsh(self): - """ - Allows the user to get a gmsh object from the assembly, respecting assembly part names and face - tags, but have more control over how it is meshed. - """ - gmsh.initialize() - gmsh.option.setNumber("General.Terminal", 0) - gmsh.model.add("coil_assembly") - - # The mesh volume and surface ids should line up with the order of solids and faces in the assembly - vol_id = 1 - surface_id = 1 +volumes = {} +volume_map = {} - # Tracks multi-surface physical groups - multi_material_groups = {} - surface_groups = {} +# Holds the collection of individual faces that are tagged +tagged_faces = {} - # Holds the collection of individual faces that are tagged - tagged_faces = {} +# Tracks multi-surface physical groups +multi_material_groups = {} +surface_groups = {} - for obj, name, loc, _ in self: - # CadQuery assembly code prepends a UUID to names sometimes that we do not need - short_name = name.split("/")[-1] - # Separate tagged faces by solid since they might be duplicates - tagged_faces[short_name] = {} +def extract_subshape_names(assy, name=None): + """ + Extracts any subshape names from the current assembly. + """ + global tagged_faces + + # We only want the last part of the name parent path + short_name = name.split("/")[-1] + + # Try extracting via names and layers + if assy._subshape_names or assy._subshape_layers: + # Make sure the entry for the assembly child exists + if short_name not in tagged_faces: + tagged_faces[short_name] = {} + + # Step through the subshape names and layers together + combined_subshapes = assy._subshape_names | assy._subshape_layers + for subshape, subshape_tag in combined_subshapes.items(): + # Create a new list for tag if it does not already exist + if subshape_tag in tagged_faces[short_name]: + tagged_faces[short_name][subshape_tag].append(subshape) + else: + tagged_faces[short_name][subshape_tag] = [subshape] + + # Check for face tags + if assy.objects[short_name].obj: + for tag, wp in assy.objects[short_name].obj.ctx.tags.items(): + # Make sure the entry for the assembly child exists + if short_name not in tagged_faces: + tagged_faces[short_name] = {} - # Extract the tagged faces and make sure they are in the appropriate relative locations - # in the assembly. Tags can hold multiple faces, so we have to extract all of them. - for tag, wp in self.objects[short_name].obj.ctx.tags.items(): for face in wp.faces().all(): - # Check to see if we have found this tag before (multi-material tag) + # Create a new list for tag if it does not already exist if tag in tagged_faces[short_name]: tagged_faces[short_name][tag].append(face.val()) else: tagged_faces[short_name][tag] = [face.val()] - # Extract the tagged faces that have been added by the addSubshape method of cq.Assembly - if self._subshape_names: - for subshape, subshape_tag in self._subshape_names.items(): - if subshape_tag in tagged_faces[short_name]: - # Check to see if this is a duplicate - if subshape in tagged_faces[short_name][subshape_tag]: - print( - f"WARNING: Duplicate subshape found for tag {subshape_tag}." - ) - - tagged_faces[short_name][subshape_tag].append(subshape) - else: - tagged_faces[short_name][subshape_tag] = [subshape] - - # Extract the tagged faces that have been added by the addSubshape method of cq.Assembly - if self._subshape_layers: - for subshape, subshape_tag in self._subshape_layers.items(): - if subshape_tag in tagged_faces[short_name]: - # Check to see if this is a duplicate - if subshape in tagged_faces[short_name][subshape_tag]: - print( - f"WARNING: Duplicate subshape found for tag {subshape_tag}." - ) - - tagged_faces[short_name][subshape_tag].append(subshape) + # Recurse through the assembly children + for child in assy.children: + extract_subshape_names(child, child.name) + + +def add_solid_to_mesh(gmsh, solid, name): + """ + Adds a given CadQuery solid to the gmsh mesh. + """ + global vol_id, volumes, volume_map + + with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: + solid.exportBrep(temp_file.name) + dim_tags = gmsh.model.occ.importShapes(temp_file.name) + + for dim, tag in dim_tags: + # We only want volumes in this pass + if dim == 3: + # Initialize the list holding the volume entities, if needed + if tag in volumes.keys(): + volumes[tag].append((3, tag)) else: - tagged_faces[short_name][subshape_tag] = [subshape] + volumes[tag] = [] + volumes[tag].append((3, tag)) + volume_map[tag] = name + + # Move to the next volume ID + vol_id += 1 + + +def add_faces_to_mesh(gmsh, solid, name, loc=None): + global surface_id, multi_material_groups, surface_groups + + # If the current solid has no tagged faces, there is nothing to do + if name not in tagged_faces.keys(): + return + + # All the faces in the current part should be added to the mesh + for face in solid.Faces(): + # Face name can be based on a tag, or just be a generic name + found_tag = False + + # + # Handle tagged faces + # Step through the faces in the solid and check them against all the tagged faces + # + for tag, tag_faces in tagged_faces[name].items(): + for tag_face in tag_faces: + # Move the face to the correct location in the assembly + if loc: + tag_face = tag_face.moved(loc) + + # If OpenCASCADE says the faces are the same, we have a match for the tag + if TopoDS_Shape.IsEqual(face.wrapped, tag_face.wrapped): + # Make sure a generic surface is not added for this face + found_tag = True + + # Find out if this is a multi-material tag + if tag.startswith("~"): + # Set the surface name to be the name of the tag without the ~ + group_name = tag.replace("~", "").split("-")[0] + + # Add this face to the multi-material group + if group_name in multi_material_groups: + multi_material_groups[group_name].append(surface_id) + else: + multi_material_groups[group_name] = [surface_id] + else: + # We want to track all surfaces that might be in a tag group + cur_tag_name = f"{name}_{tag}" + if cur_tag_name in surface_groups: + surface_groups[cur_tag_name].append(surface_id) + else: + surface_groups[cur_tag_name] = [surface_id] + + # If the solid does not have any tagged faces, add them to a generic physical group + if not found_tag: + # Generate a unique name for the surface and set it on the physical group + face_name = f"{name}_surface_{surface_id}" + ps = gmsh.model.addPhysicalGroup(2, [surface_id]) + gmsh.model.setPhysicalName(2, ps, f"{face_name}") + gmsh.model.occ.synchronize() + + # Make sure to move to the next surface ID + surface_id += 1 + + gmsh.model.occ.synchronize() + + +def get_gmsh(self, imprint=True): + """ + Allows the user to get a gmsh object from the assembly, respecting assembly part names and face + tags, but have more control over how it is meshed. This method makes sure the mesh is conformal. + """ + global vol_id, surface_id, volumes, volume_map, tagged_faces, multi_material_groups, surface_groups + + # Reset global state for each call + vol_id = 1 + surface_id = 1 + volumes = {} + volume_map = {} + tagged_faces = {} + multi_material_groups = {} + surface_groups = {} + + gmsh.initialize() + gmsh.option.setNumber( + "General.Terminal", 0 + ) # Make sure this is 0 for production for clean stdout + gmsh.model.add("assembly") + + # Get all of the subshapes and their corresponding names/positions + extract_subshape_names(self, self.name) + + # Imprint the assembly + imprinted_assembly, imprinted_solids_with_orginal_ids = ( + cq.occ_impl.assembly.imprint(self) + ) + + # Handle the imprinted assembly + if imprint: + for solid, name in imprinted_solids_with_orginal_ids.items(): + # Get just the name of the current assembly + short_name = name[0].split("/")[-1] - # All the solids in the current part should be added to the mesh - for s in obj.moved(loc).Solids(): # Add the current solid to the mesh - with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: - s.exportBrep(temp_file.name) - ps = gmsh.model.occ.importShapes(temp_file.name) - - # TODO find a way to check if the OCC in gmsh is compatible with the - # OCC in CadQuery. When pip installed they tend to be incompatible - # and this importShapesNativePointer will seg fault. When both - # packages are conda installed the importShapesNativePointer works. - # Work around that works in both cases is to write a brep and import - # it into gmsh. This is slower but works in all cases. - # gmsh.model.occ.importShapesNativePointer(s.wrapped._address()) + add_solid_to_mesh(gmsh, solid, short_name) - gmsh.model.occ.synchronize() + # Add faces to the mesh and handle tagged faces + add_faces_to_mesh(gmsh, solid, short_name, None) + + # Handle the non-imprinted assembly + else: + # Step through all of the solids in the assembly + for obj, name, loc, _ in self: + # Get just the name of the current assembly + short_name = name.split("/")[-1] - # Technically, importShapes could import multiple entities/dimensions, so filter those - vol_ents = [] - for p in ps: - if p[0] == 3: - vol_ents.append(p[1]) - - # Set the physical name to be the part name in the assembly for all the solids - ps2 = gmsh.model.addPhysicalGroup(3, vol_ents) - gmsh.model.setPhysicalName(3, ps2, f"{name.split('/')[-1]}") - - # All the faces in the current part should be added to the mesh - for face in s.Faces(): - # Face name can be based on a tag, or just be a generic name - found_tag = False - - # - # Handle tagged faces - # Step through the faces in the solid and check them against all the tagged faces - # - for tag, tag_faces in tagged_faces[short_name].items(): - for tag_face in tag_faces: - # Move the face to the correct location in the assembly - tag_face = tag_face.moved(loc) - - # If OpenCASCADE says the faces are the same, we have a match for the tag - if TopoDS_Shape.IsEqual(face.wrapped, tag_face.wrapped): - # Make sure a generic surface is not added for this face - found_tag = True - - # Find out if this is a multi-material tag - if tag.startswith("~"): - # Set the surface name to be the name of the tag without the ~ - group_name = tag.replace("~", "").split("-")[0] - - # Add this face to the multi-material group - if group_name in multi_material_groups: - multi_material_groups[group_name].append(surface_id) - else: - multi_material_groups[group_name] = [surface_id] - else: - # We want to track all surfaces that might be in a tag group - cur_tag_name = f"{short_name}_{tag}" - if cur_tag_name in surface_groups: - print( - "Append: ", cur_tag_name, short_name, surface_id - ) - surface_groups[cur_tag_name].append(surface_id) - else: - print("New: ", cur_tag_name, short_name, surface_id) - surface_groups[cur_tag_name] = [surface_id] - - # If no tag was found, set a physical group generic name - if not found_tag: - face_name = f"{short_name}_surface_{surface_id}" - ps = gmsh.model.addPhysicalGroup(2, [surface_id]) - gmsh.model.setPhysicalName(2, ps, f"{face_name}") - - # Move to the next surface id - surface_id += 1 - - # Move to the next volume id - vol_id += 1 + for solid in obj.moved(loc).Solids(): + # Add the current solid to the mesh + add_solid_to_mesh(gmsh, solid, short_name) + + # Add faces to the mesh and handle tagged faces + add_faces_to_mesh(gmsh, solid, short_name, loc) + + # Step through each of the volumes and add physical groups for each + for volume_id in volumes.keys(): + ps = gmsh.model.addPhysicalGroup(3, volumes[volume_id][0]) + gmsh.model.setPhysicalName(3, ps, f"{volume_map[volume_id]}") + gmsh.model.occ.synchronize() # Handle tagged surface groups for t_name, surf_group in surface_groups.items(): ps = gmsh.model.addPhysicalGroup(2, surf_group) gmsh.model.setPhysicalName(2, ps, t_name) + gmsh.model.occ.synchronize() # Handle multi-material tags for group_name, mm_group in multi_material_groups.items(): ps = gmsh.model.addPhysicalGroup(2, mm_group) gmsh.model.setPhysicalName(2, ps, f"{group_name}") + gmsh.model.occ.synchronize() gmsh.model.occ.synchronize() return gmsh +def get_tagged_gmsh(self): + """ + Allows the user to get a gmsh object from the assembly, respecting assembly part names and face + tags, but have more control over how it is meshed. + """ + + gmsh = get_gmsh(self, imprint=False) + + # gmsh.initialize() + # gmsh.option.setNumber("General.Terminal", 0) + # gmsh.model.add("assembly") + + # # The mesh volume and surface ids should line up with the order of solids and faces in the assembly + # vol_id = 1 + # surface_id = 1 + + # # Tracks multi-surface physical groups + # multi_material_groups = {} + # surface_groups = {} + + # # Holds the collection of individual faces that are tagged + # tagged_faces = {} + + # for obj, name, loc, _ in self: + # # CadQuery assembly code prepends a UUID to names sometimes that we do not need + # short_name = name.split("/")[-1] + + # # Separate tagged faces by solid since they might be duplicates + # tagged_faces[short_name] = {} + + # # Extract the tagged faces and make sure they are in the appropriate relative locations + # # in the assembly. Tags can hold multiple faces, so we have to extract all of them. + # for tag, wp in self.objects[short_name].obj.ctx.tags.items(): + # for face in wp.faces().all(): + # # Check to see if we have found this tag before (multi-material tag) + # if tag in tagged_faces[short_name]: + # tagged_faces[short_name][tag].append(face.val()) + # else: + # tagged_faces[short_name][tag] = [face.val()] + + # # Extract the tagged faces that have been added by the addSubshape method of cq.Assembly + # if self._subshape_names: + # for subshape, subshape_tag in self._subshape_names.items(): + # if subshape_tag in tagged_faces[short_name]: + # # Check to see if this is a duplicate + # if subshape in tagged_faces[short_name][subshape_tag]: + # print( + # f"WARNING: Duplicate subshape found for tag {subshape_tag}." + # ) + + # tagged_faces[short_name][subshape_tag].append(subshape) + # else: + # tagged_faces[short_name][subshape_tag] = [subshape] + + # # Extract the tagged faces that have been added by the addSubshape method of cq.Assembly + # if self._subshape_layers: + # for subshape, subshape_tag in self._subshape_layers.items(): + # if subshape_tag in tagged_faces[short_name]: + # # Check to see if this is a duplicate + # if subshape in tagged_faces[short_name][subshape_tag]: + # print( + # f"WARNING: Duplicate subshape found for tag {subshape_tag}." + # ) + + # tagged_faces[short_name][subshape_tag].append(subshape) + # else: + # tagged_faces[short_name][subshape_tag] = [subshape] + + # # All the solids in the current part should be added to the mesh + # for s in obj.moved(loc).Solids(): + # # Add the current solid to the mesh + # with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: + # s.exportBrep(temp_file.name) + # ps = gmsh.model.occ.importShapes(temp_file.name) + + # # TODO find a way to check if the OCC in gmsh is compatible with the + # # OCC in CadQuery. When pip installed they tend to be incompatible + # # and this importShapesNativePointer will seg fault. When both + # # packages are conda installed the importShapesNativePointer works. + # # Work around that works in both cases is to write a brep and import + # # it into gmsh. This is slower but works in all cases. + # # gmsh.model.occ.importShapesNativePointer(s.wrapped._address()) + + # gmsh.model.occ.synchronize() + + # # Technically, importShapes could import multiple entities/dimensions, so filter those + # vol_ents = [] + # for p in ps: + # if p[0] == 3: + # vol_ents.append(p[1]) + + # # Set the physical name to be the part name in the assembly for all the solids + # ps2 = gmsh.model.addPhysicalGroup(3, vol_ents) + # gmsh.model.setPhysicalName(3, ps2, f"{name.split('/')[-1]}") + + # # All the faces in the current part should be added to the mesh + # for face in s.Faces(): + # # Face name can be based on a tag, or just be a generic name + # found_tag = False + + # # + # # Handle tagged faces + # # Step through the faces in the solid and check them against all the tagged faces + # # + # for tag, tag_faces in tagged_faces[short_name].items(): + # for tag_face in tag_faces: + # # Move the face to the correct location in the assembly + # tag_face = tag_face.moved(loc) + + # # If OpenCASCADE says the faces are the same, we have a match for the tag + # if TopoDS_Shape.IsEqual(face.wrapped, tag_face.wrapped): + # # Make sure a generic surface is not added for this face + # found_tag = True + + # # Find out if this is a multi-material tag + # if tag.startswith("~"): + # # Set the surface name to be the name of the tag without the ~ + # group_name = tag.replace("~", "").split("-")[0] + + # # Add this face to the multi-material group + # if group_name in multi_material_groups: + # multi_material_groups[group_name].append(surface_id) + # else: + # multi_material_groups[group_name] = [surface_id] + # else: + # # We want to track all surfaces that might be in a tag group + # cur_tag_name = f"{short_name}_{tag}" + # if cur_tag_name in surface_groups: + # print( + # "Append: ", cur_tag_name, short_name, surface_id + # ) + # surface_groups[cur_tag_name].append(surface_id) + # else: + # print("New: ", cur_tag_name, short_name, surface_id) + # surface_groups[cur_tag_name] = [surface_id] + + # # If no tag was found, set a physical group generic name + # if not found_tag: + # face_name = f"{short_name}_surface_{surface_id}" + # ps = gmsh.model.addPhysicalGroup(2, [surface_id]) + # gmsh.model.setPhysicalName(2, ps, f"{face_name}") + + # # Move to the next surface id + # surface_id += 1 + + # # Move to the next volume id + # vol_id += 1 + + # # Handle tagged surface groups + # for t_name, surf_group in surface_groups.items(): + # ps = gmsh.model.addPhysicalGroup(2, surf_group) + # gmsh.model.setPhysicalName(2, ps, t_name) + + # # Handle multi-material tags + # for group_name, mm_group in multi_material_groups.items(): + # ps = gmsh.model.addPhysicalGroup(2, mm_group) + # gmsh.model.setPhysicalName(2, ps, f"{group_name}") + + # gmsh.model.occ.synchronize() + + return gmsh + + def assembly_to_gmsh(self, mesh_path="tagged_mesh.msh"): """ Pack the assembly into a gmsh object, respecting assembly part names and face tags when creating @@ -187,115 +410,117 @@ def get_imprinted_gmsh(self): Allows the user to get a gmsh object from the assembly, with the assembly being imprinted. """ - # Initialize gmsh and create a new model - gmsh.initialize() - gmsh.option.setNumber("General.Terminal", 0) - gmsh.model.add("assembly") - - # The mesh volume and surface ids should line up with the order of solids and faces in the assembly - vol_id = 1 - surface_id = 1 - - # Tracks multi-surface physical groups - multi_material_groups = {} - surface_groups = {} - - # Tracks the solids with tagged faces - tagged_faces = {} - solids_with_tagged_faces = {} - - # Imprint the assembly - imprinted_assembly, imprinted_solids_with_orginal_ids = ( - cq.occ_impl.assembly.imprint(self) - ) - - for solid, id in imprinted_solids_with_orginal_ids.items(): - # Add the current solid to the mesh - # Work-around for a segfault with in-memory passing of OCCT objects - with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: - solid.exportBrep(temp_file.name) - ps = gmsh.model.occ.importShapes(temp_file.name) - gmsh.model.occ.synchronize() + gmsh = get_gmsh(self, imprint=True) - # Technically, importShapes could import multiple entities/dimensions, so filter those - vol_ents = [] - for p in ps: - if p[0] == 3: - vol_ents.append(p[1]) - - # Set the physical name to be the part name in the assembly for all the solids - ps2 = gmsh.model.addPhysicalGroup(3, vol_ents) - gmsh.model.setPhysicalName(3, ps2, f"{id[0].split('/')[-1]}") - - # Get the original assembly part - object_name = id[0].split("/")[-1] - assembly_part = self.objects[object_name] - - # Collect any tags from the part - for tag, wp in assembly_part.obj.ctx.tags.items(): - tagged_face = wp.faces().all()[0].val() - for face in wp.faces().all(): - tagged_faces[face.val()] = tag - - # Iterate over the faces of the assembly part - for face in assembly_part.obj.faces(): - for tagged_face, tag in tagged_faces.items(): - if TopoDS_Shape.IsEqual(face.wrapped, tagged_face.wrapped): - print(f"{vol_id}_{surface_id}", tag) - solids_with_tagged_faces[f"{vol_id}_{surface_id}"] = ( - object_name, - tag, - ) - - surface_id += 1 - vol_id += 1 - - # Reset the volume and surface IDs - vol_id = 1 - surface_id = 1 - - # Step through the imprinted assembly/shape and check for tagged faces - for solid in imprinted_assembly.solids(): - for face in solid.faces().Faces(): - # Check to see if this face has been tagged - if f"{vol_id}_{surface_id}" in solids_with_tagged_faces.keys(): - short_name = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][0] - tag = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][1] - - # Find out if this is a multi-material tag - if tag.startswith("~"): - # Set the surface name to be the name of the tag without the ~ - group_name = tag.replace("~", "").split("-")[0] - - # Add this face to the multi-material group - if group_name in multi_material_groups: - multi_material_groups[group_name].append(surface_id) - else: - multi_material_groups[group_name] = [surface_id] - else: - # We want to track all surfaces that might be in a tag group - cur_tag_name = f"{short_name}_{tag}" - if cur_tag_name in surface_groups: - print("Append: ", cur_tag_name, short_name, surface_id) - surface_groups[cur_tag_name].append(surface_id) - else: - print("New: ", cur_tag_name, short_name, surface_id) - surface_groups[cur_tag_name] = [surface_id] - - surface_id += 1 - vol_id += 1 - - # Handle tagged surface groups - for t_name, surf_group in surface_groups.items(): - ps = gmsh.model.addPhysicalGroup(2, surf_group) - gmsh.model.setPhysicalName(2, ps, t_name) - - # Handle multi-material tags - for group_name, mm_group in multi_material_groups.items(): - ps = gmsh.model.addPhysicalGroup(2, mm_group) - gmsh.model.setPhysicalName(2, ps, f"{group_name}") - - gmsh.model.occ.synchronize() + # Initialize gmsh and create a new model + # gmsh.initialize() + # gmsh.option.setNumber("General.Terminal", 0) + # gmsh.model.add("assembly") + + # # The mesh volume and surface ids should line up with the order of solids and faces in the assembly + # vol_id = 1 + # surface_id = 1 + + # # Tracks multi-surface physical groups + # multi_material_groups = {} + # surface_groups = {} + + # # Tracks the solids with tagged faces + # tagged_faces = {} + # solids_with_tagged_faces = {} + + # # Imprint the assembly + # imprinted_assembly, imprinted_solids_with_orginal_ids = ( + # cq.occ_impl.assembly.imprint(self) + # ) + + # for solid, id in imprinted_solids_with_orginal_ids.items(): + # # Add the current solid to the mesh + # # Work-around for a segfault with in-memory passing of OCCT objects + # with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: + # solid.exportBrep(temp_file.name) + # ps = gmsh.model.occ.importShapes(temp_file.name) + # gmsh.model.occ.synchronize() + + # # Technically, importShapes could import multiple entities/dimensions, so filter those + # vol_ents = [] + # for p in ps: + # if p[0] == 3: + # vol_ents.append(p[1]) + + # # Set the physical name to be the part name in the assembly for all the solids + # ps2 = gmsh.model.addPhysicalGroup(3, vol_ents) + # gmsh.model.setPhysicalName(3, ps2, f"{id[0].split('/')[-1]}") + + # # Get the original assembly part + # object_name = id[0].split("/")[-1] + # assembly_part = self.objects[object_name] + + # # Collect any tags from the part + # for tag, wp in assembly_part.obj.ctx.tags.items(): + # tagged_face = wp.faces().all()[0].val() + # for face in wp.faces().all(): + # tagged_faces[face.val()] = tag + + # # Iterate over the faces of the assembly part + # for face in assembly_part.obj.faces(): + # for tagged_face, tag in tagged_faces.items(): + # if TopoDS_Shape.IsEqual(face.wrapped, tagged_face.wrapped): + # print(f"{vol_id}_{surface_id}", tag) + # solids_with_tagged_faces[f"{vol_id}_{surface_id}"] = ( + # object_name, + # tag, + # ) + + # surface_id += 1 + # vol_id += 1 + + # # Reset the volume and surface IDs + # vol_id = 1 + # surface_id = 1 + + # # Step through the imprinted assembly/shape and check for tagged faces + # for solid in imprinted_assembly.solids(): + # for face in solid.faces().Faces(): + # # Check to see if this face has been tagged + # if f"{vol_id}_{surface_id}" in solids_with_tagged_faces.keys(): + # short_name = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][0] + # tag = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][1] + + # # Find out if this is a multi-material tag + # if tag.startswith("~"): + # # Set the surface name to be the name of the tag without the ~ + # group_name = tag.replace("~", "").split("-")[0] + + # # Add this face to the multi-material group + # if group_name in multi_material_groups: + # multi_material_groups[group_name].append(surface_id) + # else: + # multi_material_groups[group_name] = [surface_id] + # else: + # # We want to track all surfaces that might be in a tag group + # cur_tag_name = f"{short_name}_{tag}" + # if cur_tag_name in surface_groups: + # print("Append: ", cur_tag_name, short_name, surface_id) + # surface_groups[cur_tag_name].append(surface_id) + # else: + # print("New: ", cur_tag_name, short_name, surface_id) + # surface_groups[cur_tag_name] = [surface_id] + + # surface_id += 1 + # vol_id += 1 + + # # Handle tagged surface groups + # for t_name, surf_group in surface_groups.items(): + # ps = gmsh.model.addPhysicalGroup(2, surf_group) + # gmsh.model.setPhysicalName(2, ps, t_name) + + # # Handle multi-material tags + # for group_name, mm_group in multi_material_groups.items(): + # ps = gmsh.model.addPhysicalGroup(2, mm_group) + # gmsh.model.setPhysicalName(2, ps, f"{group_name}") + + # gmsh.model.occ.synchronize() return gmsh @@ -319,6 +544,7 @@ def assembly_to_imprinted_gmsh(self, mesh_path="tagged_mesh.msh"): # Patch the new assembly functions into CadQuery's importers package cq.Assembly.assemblyToGmsh = assembly_to_gmsh cq.Assembly.saveToGmsh = assembly_to_gmsh # Alias name that works better on cq.Assembly -cq.Assembly.getTaggedGmsh = get_tagged_gmsh cq.Assembly.assemblyToImprintedGmsh = assembly_to_imprinted_gmsh +cq.Assembly.getTaggedGmsh = get_tagged_gmsh cq.Assembly.getImprintedGmsh = get_imprinted_gmsh +cq.Assembly.getGmsh = get_gmsh diff --git a/tests/sample_assemblies.py b/tests/sample_assemblies.py index ef01c81..4e546c6 100644 --- a/tests/sample_assemblies.py +++ b/tests/sample_assemblies.py @@ -1,6 +1,38 @@ import cadquery as cq +def generate_nested_spheres(): + """ + Used for confirming conformal meshing. + """ + + # Generate the simple assembly of two nested spheres + box_cutter = cq.Workplane("XY").moveTo(0, 5).box(20, 10, 20) + inner_sphere = cq.Workplane("XY").sphere(6).cut(box_cutter) + middle_sphere = cq.Workplane("XY").sphere(6.1).cut(box_cutter).cut(inner_sphere) + + assy = cq.Assembly() + assy.add(inner_sphere, name="inner_sphere") + assy.add(middle_sphere, name="middle_sphere") + + return assy + + +def generate_touching_boxes(): + """ + Generates an assembly of two cubes which touch on one face. + """ + + cube_1 = cq.Workplane().box(10, 10, 10) + cube_2 = cq.Workplane().transformed(offset=(10, 0, 0)).box(10, 10, 10) + + assy = cq.Assembly() + assy.add(cube_1, name="left_cube") + assy.add(cube_2, name="right_cube") + + return assy + + def generate_nested_boxes(): """ Generates a simple assembly of two cubes where one is nested inside the other. diff --git a/tests/test_meshes.py b/tests/test_meshes.py index 65cb484..5e77a5a 100644 --- a/tests/test_meshes.py +++ b/tests/test_meshes.py @@ -1,6 +1,8 @@ import gmsh import assembly_mesh_plugin from tests.sample_assemblies import ( + generate_nested_spheres, + generate_touching_boxes, generate_nested_boxes, generate_simple_nested_boxes, generate_test_cross_section, @@ -24,16 +26,22 @@ def test_simple_assembly(): gmsh.open("tagged_mesh.msh") - # Check the solids for the correct tags + # Make sure that there are physical groups for the volumes physical_groups = gmsh.model.getPhysicalGroups(3) + assert len(physical_groups) > 0, "There should be some physical groups for volumes" + + # Check the solids for the correct tags for group in physical_groups: # Get the name for the current volume cur_name = gmsh.model.getPhysicalName(3, group[1]) assert cur_name in ["shell", "insert"] - # Check the surfaces for the correct tags + # Check to make sure there are physical groups for the surfaces physical_groups = gmsh.model.getPhysicalGroups(2) + assert len(physical_groups) > 0, "There should be some physical groups for surfaces" + + # Check the surfaces for the correct tags for group in physical_groups: # Get the name for this group cur_name = gmsh.model.getPhysicalName(2, group[1]) @@ -60,16 +68,22 @@ def test_subshape_assembly(): gmsh.open("tagged_subshape_mesh.msh") - # Check the solids for the correct tags + # Make sure that there are physical groups for the volumes physical_groups = gmsh.model.getPhysicalGroups(3) + assert len(physical_groups) > 0, "There should be some physical groups for volumes" + + # Check the solids/volumes for the correct tags for group in physical_groups: # Get the name for the current volume cur_name = gmsh.model.getPhysicalName(3, group[1]) assert cur_name in ["cube_1"] - # Check the surfaces for the correct tags + # Check to make sure there are physical groups for the surfaces physical_groups = gmsh.model.getPhysicalGroups(2) + assert len(physical_groups) > 0, "There should be some physical groups for surfaces" + + # Check the surfaces for the correct tags for group in physical_groups: # Get the name for this group cur_name = gmsh.model.getPhysicalName(2, group[1]) @@ -91,16 +105,22 @@ def test_imprinted_assembly(): gmsh.open("tagged_imprinted_mesh.msh") - # Check the solids for the correct tags + # Make sure that there are physical groups for the volumes physical_groups = gmsh.model.getPhysicalGroups(3) + assert len(physical_groups) > 0, "There should be some physical groups for volumes" + + # Check the solids for the correct tags for group in physical_groups: # Get the name for the current volume cur_name = gmsh.model.getPhysicalName(3, group[1]) assert cur_name in ["shell", "insert"] - # Check the surfaces for the correct tags + # Check to make sure there are physical groups for the surfaces physical_groups = gmsh.model.getPhysicalGroups(2) + assert len(physical_groups) > 0, "There should be some physical groups for surfaces" + + # Check the surfaces for the correct tags for group in physical_groups: # Get the name for this group cur_name = gmsh.model.getPhysicalName(2, group[1]) @@ -110,3 +130,51 @@ def test_imprinted_assembly(): continue assert cur_name in ["shell_inner-right", "insert_outer-right", "in_contact"] + + +def test_conformal_mesh_compliance(): + """ + Tests to make sure the meshing process produces a conformal mesh. + """ + + def _check_physical_groups(): + # Make sure that there are physical groups for the volumes + physical_groups = gmsh.model.getPhysicalGroups(3) + assert ( + len(physical_groups) > 0 + ), "There should be some physical groups for volumes" + + # Check the solids for the correct tags + for group in physical_groups: + # Get the name for the current volume + cur_name = gmsh.model.getPhysicalName(3, group[1]) + + assert cur_name in ["inner_sphere", "middle_sphere"] + + # Check to make sure there are physical groups for the surfaces + physical_groups = gmsh.model.getPhysicalGroups(2) + # assert len(physical_groups) > 0, "There should be some physical groups for surfaces" + + # Create a basic assembly + assy = generate_nested_spheres() + + # + # Go through the entire process with an imprinted assembly. + # + gmsh = assy.getGmsh(imprint=True) + gmsh.model.mesh.generate(3) + + # Ensure that there are physical groups and that they have the right names + _check_physical_groups() + + # + # Go othrough the entire process again with a non-imprinted assembly. + # + gmsh = assy.getGmsh(imprint=False) + gmsh.model.mesh.generate(3) + + # Ensure that there are physical groups + _check_physical_groups() + + # assert False + # gmsh.fltk.run() From 80c18f4d36c664733825a9f86769a6d7ecb6811d Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 2 Dec 2025 09:54:55 -0500 Subject: [PATCH 2/5] Removed specific conformal check for now --- tests/test_meshes.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/test_meshes.py b/tests/test_meshes.py index 5e77a5a..3707754 100644 --- a/tests/test_meshes.py +++ b/tests/test_meshes.py @@ -132,9 +132,9 @@ def test_imprinted_assembly(): assert cur_name in ["shell_inner-right", "insert_outer-right", "in_contact"] -def test_conformal_mesh_compliance(): +def test_nested_sphere_assembly(): """ - Tests to make sure the meshing process produces a conformal mesh. + Tests to make sure the the nested sphere example works. """ def _check_physical_groups(): @@ -151,10 +151,6 @@ def _check_physical_groups(): assert cur_name in ["inner_sphere", "middle_sphere"] - # Check to make sure there are physical groups for the surfaces - physical_groups = gmsh.model.getPhysicalGroups(2) - # assert len(physical_groups) > 0, "There should be some physical groups for surfaces" - # Create a basic assembly assy = generate_nested_spheres() @@ -175,6 +171,3 @@ def _check_physical_groups(): # Ensure that there are physical groups _check_physical_groups() - - # assert False - # gmsh.fltk.run() From 8fd58cfe99db666f5f51911e3bb3305ae3b8e14a Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 2 Dec 2025 09:56:55 -0500 Subject: [PATCH 3/5] Removed commented code --- assembly_mesh_plugin/plugin.py | 262 --------------------------------- 1 file changed, 262 deletions(-) diff --git a/assembly_mesh_plugin/plugin.py b/assembly_mesh_plugin/plugin.py index 391a97e..66e2f35 100644 --- a/assembly_mesh_plugin/plugin.py +++ b/assembly_mesh_plugin/plugin.py @@ -233,158 +233,6 @@ def get_tagged_gmsh(self): gmsh = get_gmsh(self, imprint=False) - # gmsh.initialize() - # gmsh.option.setNumber("General.Terminal", 0) - # gmsh.model.add("assembly") - - # # The mesh volume and surface ids should line up with the order of solids and faces in the assembly - # vol_id = 1 - # surface_id = 1 - - # # Tracks multi-surface physical groups - # multi_material_groups = {} - # surface_groups = {} - - # # Holds the collection of individual faces that are tagged - # tagged_faces = {} - - # for obj, name, loc, _ in self: - # # CadQuery assembly code prepends a UUID to names sometimes that we do not need - # short_name = name.split("/")[-1] - - # # Separate tagged faces by solid since they might be duplicates - # tagged_faces[short_name] = {} - - # # Extract the tagged faces and make sure they are in the appropriate relative locations - # # in the assembly. Tags can hold multiple faces, so we have to extract all of them. - # for tag, wp in self.objects[short_name].obj.ctx.tags.items(): - # for face in wp.faces().all(): - # # Check to see if we have found this tag before (multi-material tag) - # if tag in tagged_faces[short_name]: - # tagged_faces[short_name][tag].append(face.val()) - # else: - # tagged_faces[short_name][tag] = [face.val()] - - # # Extract the tagged faces that have been added by the addSubshape method of cq.Assembly - # if self._subshape_names: - # for subshape, subshape_tag in self._subshape_names.items(): - # if subshape_tag in tagged_faces[short_name]: - # # Check to see if this is a duplicate - # if subshape in tagged_faces[short_name][subshape_tag]: - # print( - # f"WARNING: Duplicate subshape found for tag {subshape_tag}." - # ) - - # tagged_faces[short_name][subshape_tag].append(subshape) - # else: - # tagged_faces[short_name][subshape_tag] = [subshape] - - # # Extract the tagged faces that have been added by the addSubshape method of cq.Assembly - # if self._subshape_layers: - # for subshape, subshape_tag in self._subshape_layers.items(): - # if subshape_tag in tagged_faces[short_name]: - # # Check to see if this is a duplicate - # if subshape in tagged_faces[short_name][subshape_tag]: - # print( - # f"WARNING: Duplicate subshape found for tag {subshape_tag}." - # ) - - # tagged_faces[short_name][subshape_tag].append(subshape) - # else: - # tagged_faces[short_name][subshape_tag] = [subshape] - - # # All the solids in the current part should be added to the mesh - # for s in obj.moved(loc).Solids(): - # # Add the current solid to the mesh - # with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: - # s.exportBrep(temp_file.name) - # ps = gmsh.model.occ.importShapes(temp_file.name) - - # # TODO find a way to check if the OCC in gmsh is compatible with the - # # OCC in CadQuery. When pip installed they tend to be incompatible - # # and this importShapesNativePointer will seg fault. When both - # # packages are conda installed the importShapesNativePointer works. - # # Work around that works in both cases is to write a brep and import - # # it into gmsh. This is slower but works in all cases. - # # gmsh.model.occ.importShapesNativePointer(s.wrapped._address()) - - # gmsh.model.occ.synchronize() - - # # Technically, importShapes could import multiple entities/dimensions, so filter those - # vol_ents = [] - # for p in ps: - # if p[0] == 3: - # vol_ents.append(p[1]) - - # # Set the physical name to be the part name in the assembly for all the solids - # ps2 = gmsh.model.addPhysicalGroup(3, vol_ents) - # gmsh.model.setPhysicalName(3, ps2, f"{name.split('/')[-1]}") - - # # All the faces in the current part should be added to the mesh - # for face in s.Faces(): - # # Face name can be based on a tag, or just be a generic name - # found_tag = False - - # # - # # Handle tagged faces - # # Step through the faces in the solid and check them against all the tagged faces - # # - # for tag, tag_faces in tagged_faces[short_name].items(): - # for tag_face in tag_faces: - # # Move the face to the correct location in the assembly - # tag_face = tag_face.moved(loc) - - # # If OpenCASCADE says the faces are the same, we have a match for the tag - # if TopoDS_Shape.IsEqual(face.wrapped, tag_face.wrapped): - # # Make sure a generic surface is not added for this face - # found_tag = True - - # # Find out if this is a multi-material tag - # if tag.startswith("~"): - # # Set the surface name to be the name of the tag without the ~ - # group_name = tag.replace("~", "").split("-")[0] - - # # Add this face to the multi-material group - # if group_name in multi_material_groups: - # multi_material_groups[group_name].append(surface_id) - # else: - # multi_material_groups[group_name] = [surface_id] - # else: - # # We want to track all surfaces that might be in a tag group - # cur_tag_name = f"{short_name}_{tag}" - # if cur_tag_name in surface_groups: - # print( - # "Append: ", cur_tag_name, short_name, surface_id - # ) - # surface_groups[cur_tag_name].append(surface_id) - # else: - # print("New: ", cur_tag_name, short_name, surface_id) - # surface_groups[cur_tag_name] = [surface_id] - - # # If no tag was found, set a physical group generic name - # if not found_tag: - # face_name = f"{short_name}_surface_{surface_id}" - # ps = gmsh.model.addPhysicalGroup(2, [surface_id]) - # gmsh.model.setPhysicalName(2, ps, f"{face_name}") - - # # Move to the next surface id - # surface_id += 1 - - # # Move to the next volume id - # vol_id += 1 - - # # Handle tagged surface groups - # for t_name, surf_group in surface_groups.items(): - # ps = gmsh.model.addPhysicalGroup(2, surf_group) - # gmsh.model.setPhysicalName(2, ps, t_name) - - # # Handle multi-material tags - # for group_name, mm_group in multi_material_groups.items(): - # ps = gmsh.model.addPhysicalGroup(2, mm_group) - # gmsh.model.setPhysicalName(2, ps, f"{group_name}") - - # gmsh.model.occ.synchronize() - return gmsh @@ -412,116 +260,6 @@ def get_imprinted_gmsh(self): gmsh = get_gmsh(self, imprint=True) - # Initialize gmsh and create a new model - # gmsh.initialize() - # gmsh.option.setNumber("General.Terminal", 0) - # gmsh.model.add("assembly") - - # # The mesh volume and surface ids should line up with the order of solids and faces in the assembly - # vol_id = 1 - # surface_id = 1 - - # # Tracks multi-surface physical groups - # multi_material_groups = {} - # surface_groups = {} - - # # Tracks the solids with tagged faces - # tagged_faces = {} - # solids_with_tagged_faces = {} - - # # Imprint the assembly - # imprinted_assembly, imprinted_solids_with_orginal_ids = ( - # cq.occ_impl.assembly.imprint(self) - # ) - - # for solid, id in imprinted_solids_with_orginal_ids.items(): - # # Add the current solid to the mesh - # # Work-around for a segfault with in-memory passing of OCCT objects - # with tempfile.NamedTemporaryFile(suffix=".brep") as temp_file: - # solid.exportBrep(temp_file.name) - # ps = gmsh.model.occ.importShapes(temp_file.name) - # gmsh.model.occ.synchronize() - - # # Technically, importShapes could import multiple entities/dimensions, so filter those - # vol_ents = [] - # for p in ps: - # if p[0] == 3: - # vol_ents.append(p[1]) - - # # Set the physical name to be the part name in the assembly for all the solids - # ps2 = gmsh.model.addPhysicalGroup(3, vol_ents) - # gmsh.model.setPhysicalName(3, ps2, f"{id[0].split('/')[-1]}") - - # # Get the original assembly part - # object_name = id[0].split("/")[-1] - # assembly_part = self.objects[object_name] - - # # Collect any tags from the part - # for tag, wp in assembly_part.obj.ctx.tags.items(): - # tagged_face = wp.faces().all()[0].val() - # for face in wp.faces().all(): - # tagged_faces[face.val()] = tag - - # # Iterate over the faces of the assembly part - # for face in assembly_part.obj.faces(): - # for tagged_face, tag in tagged_faces.items(): - # if TopoDS_Shape.IsEqual(face.wrapped, tagged_face.wrapped): - # print(f"{vol_id}_{surface_id}", tag) - # solids_with_tagged_faces[f"{vol_id}_{surface_id}"] = ( - # object_name, - # tag, - # ) - - # surface_id += 1 - # vol_id += 1 - - # # Reset the volume and surface IDs - # vol_id = 1 - # surface_id = 1 - - # # Step through the imprinted assembly/shape and check for tagged faces - # for solid in imprinted_assembly.solids(): - # for face in solid.faces().Faces(): - # # Check to see if this face has been tagged - # if f"{vol_id}_{surface_id}" in solids_with_tagged_faces.keys(): - # short_name = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][0] - # tag = solids_with_tagged_faces[f"{vol_id}_{surface_id}"][1] - - # # Find out if this is a multi-material tag - # if tag.startswith("~"): - # # Set the surface name to be the name of the tag without the ~ - # group_name = tag.replace("~", "").split("-")[0] - - # # Add this face to the multi-material group - # if group_name in multi_material_groups: - # multi_material_groups[group_name].append(surface_id) - # else: - # multi_material_groups[group_name] = [surface_id] - # else: - # # We want to track all surfaces that might be in a tag group - # cur_tag_name = f"{short_name}_{tag}" - # if cur_tag_name in surface_groups: - # print("Append: ", cur_tag_name, short_name, surface_id) - # surface_groups[cur_tag_name].append(surface_id) - # else: - # print("New: ", cur_tag_name, short_name, surface_id) - # surface_groups[cur_tag_name] = [surface_id] - - # surface_id += 1 - # vol_id += 1 - - # # Handle tagged surface groups - # for t_name, surf_group in surface_groups.items(): - # ps = gmsh.model.addPhysicalGroup(2, surf_group) - # gmsh.model.setPhysicalName(2, ps, t_name) - - # # Handle multi-material tags - # for group_name, mm_group in multi_material_groups.items(): - # ps = gmsh.model.addPhysicalGroup(2, mm_group) - # gmsh.model.setPhysicalName(2, ps, f"{group_name}") - - # gmsh.model.occ.synchronize() - return gmsh From 0a7759abbeac38aad7ceeb027381f9523f21b0a1 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 2 Dec 2025 09:57:41 -0500 Subject: [PATCH 4/5] Rearranging a bit --- assembly_mesh_plugin/plugin.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/assembly_mesh_plugin/plugin.py b/assembly_mesh_plugin/plugin.py index 66e2f35..cff9cdb 100644 --- a/assembly_mesh_plugin/plugin.py +++ b/assembly_mesh_plugin/plugin.py @@ -236,6 +236,16 @@ def get_tagged_gmsh(self): return gmsh +def get_imprinted_gmsh(self): + """ + Allows the user to get a gmsh object from the assembly, with the assembly being imprinted. + """ + + gmsh = get_gmsh(self, imprint=True) + + return gmsh + + def assembly_to_gmsh(self, mesh_path="tagged_mesh.msh"): """ Pack the assembly into a gmsh object, respecting assembly part names and face tags when creating @@ -253,16 +263,6 @@ def assembly_to_gmsh(self, mesh_path="tagged_mesh.msh"): gmsh.finalize() -def get_imprinted_gmsh(self): - """ - Allows the user to get a gmsh object from the assembly, with the assembly being imprinted. - """ - - gmsh = get_gmsh(self, imprint=True) - - return gmsh - - def assembly_to_imprinted_gmsh(self, mesh_path="tagged_mesh.msh"): """ Exports an imprinted assembly to capture conformal meshes. From af3bc72b8816f9fd26c0b0da1d0dbb04c0b63754 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 2 Dec 2025 16:28:05 -0500 Subject: [PATCH 5/5] Fixed a bug by synchronize being called at the wrong time --- assembly_mesh_plugin/plugin.py | 6 +++--- tests/test_meshes.py | 8 ++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/assembly_mesh_plugin/plugin.py b/assembly_mesh_plugin/plugin.py index cff9cdb..3743372 100644 --- a/assembly_mesh_plugin/plugin.py +++ b/assembly_mesh_plugin/plugin.py @@ -204,21 +204,21 @@ def get_gmsh(self, imprint=True): # Step through each of the volumes and add physical groups for each for volume_id in volumes.keys(): + gmsh.model.occ.synchronize() ps = gmsh.model.addPhysicalGroup(3, volumes[volume_id][0]) gmsh.model.setPhysicalName(3, ps, f"{volume_map[volume_id]}") - gmsh.model.occ.synchronize() # Handle tagged surface groups for t_name, surf_group in surface_groups.items(): + gmsh.model.occ.synchronize() ps = gmsh.model.addPhysicalGroup(2, surf_group) gmsh.model.setPhysicalName(2, ps, t_name) - gmsh.model.occ.synchronize() # Handle multi-material tags for group_name, mm_group in multi_material_groups.items(): + gmsh.model.occ.synchronize() ps = gmsh.model.addPhysicalGroup(2, mm_group) gmsh.model.setPhysicalName(2, ps, f"{group_name}") - gmsh.model.occ.synchronize() gmsh.model.occ.synchronize() diff --git a/tests/test_meshes.py b/tests/test_meshes.py index 3707754..24485c9 100644 --- a/tests/test_meshes.py +++ b/tests/test_meshes.py @@ -141,8 +141,8 @@ def _check_physical_groups(): # Make sure that there are physical groups for the volumes physical_groups = gmsh.model.getPhysicalGroups(3) assert ( - len(physical_groups) > 0 - ), "There should be some physical groups for volumes" + len(physical_groups) == 2 + ), "There should be two physical groups for volumes" # Check the solids for the correct tags for group in physical_groups: @@ -151,6 +151,10 @@ def _check_physical_groups(): assert cur_name in ["inner_sphere", "middle_sphere"] + # Make sure we can retrieve the physical groups + inner_sphere_volume = gmsh.model.getEntitiesForPhysicalName("inner_sphere") + middle_sphere_volume = gmsh.model.getEntitiesForPhysicalName("middle_sphere") + # Create a basic assembly assy = generate_nested_spheres()