From d502e860ffb13393a55c6596f8e48d1745d1ab89 Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:50:49 +0200 Subject: [PATCH 01/12] fix: bring in fixes to mesh the cavities --- src/ansys/heart/core/models.py | 59 +++++----- src/ansys/heart/core/pre/mesher.py | 113 ++++++++++++-------- src/ansys/heart/core/utils/fluent_reader.py | 15 ++- 3 files changed, 111 insertions(+), 76 deletions(-) diff --git a/src/ansys/heart/core/models.py b/src/ansys/heart/core/models.py index 67d18add8..c3ea35efa 100644 --- a/src/ansys/heart/core/models.py +++ b/src/ansys/heart/core/models.py @@ -520,14 +520,8 @@ def mesh_volume( return self.mesh - def _mesh_fluid_volume(self, remesh_caps: bool = True): - """Generate a volume mesh of the cavities. - - Parameters - ---------- - remesh_caps : bool, optional - Flag indicating whether to remesh the caps of each cavity, by default True - """ + def _mesh_fluid_volume(self): + """Generate a volume mesh of the cavities.""" # get all relevant boundaries for the fluid cavities: substrings_include = ["endocardium", "valve-plane", "septum"] substrings_include_re = "|".join(substrings_include) @@ -555,38 +549,45 @@ def _mesh_fluid_volume(self, remesh_caps: bool = True): LOGGER.info("Meshing fluid cavities...") + # get list of fluid cavities # mesh the fluid cavities - fluid_mesh = mesher._mesh_fluid_cavities( - boundaries_fluid, caps, self.workdir, remesh_caps=remesh_caps - ) + cavity_surfaces = [ + self.mesh.get_surface(part.cavity.surface.id) for part in self.parts if part.cavity + ] + # remove caps. + cavity_surfaces = [ + SurfaceMesh(cs.threshold((0, 0), "_cap_id").extract_surface(), name=cs.name) + for cs in cavity_surfaces + ] + fluid_mesh = mesher._mesh_fluid_cavities(cavity_surfaces, self.workdir, mesh_size=1) - LOGGER.info(f"Meshed {len(fluid_mesh.cell_zones)} fluid regions...") + # LOGGER.info(f"Meshed {len(fluid_mesh.cell_zones)} fluid regions...") # add part-ids - cz_ids = np.sort([cz.id for cz in fluid_mesh.cell_zones]) + # cz_ids = np.sort([cz.id for cz in fluid_mesh.cell_zones]) # TODO: this offset is arbitrary. - offset = 10000 - new_ids = np.arange(cz_ids.shape[0]) + offset - czid_to_pid = {cz_id: new_ids[ii] for ii, cz_id in enumerate(cz_ids)} + # offset = 10000 + # new_ids = np.arange(cz_ids.shape[0]) + offset + # czid_to_pid = {cz_id: new_ids[ii] for ii, cz_id in enumerate(cz_ids)} - for cz in fluid_mesh.cell_zones: - cz.id = czid_to_pid[cz.id] + # for cz in fluid_mesh.cell_zones: + # cz.id = czid_to_pid[cz.id] - fluid_mesh._fix_negative_cells() - fluid_mesh_vtk = fluid_mesh._to_vtk(add_cells=True, add_faces=False) + # fluid_mesh._fix_negative_cells() + # fluid_mesh_vtk = fluid_mesh._to_vtk(add_cells=True, add_faces=False) - fluid_mesh_vtk.cell_data["_volume-id"] = fluid_mesh_vtk.cell_data["cell-zone-ids"] + # fluid_mesh_vtk.cell_data["part-id"] = fluid_mesh_vtk.cell_data["cell-zone-ids"] - boundaries = [ - SurfaceMesh(name=fz.name, triangles=fz.faces, nodes=fluid_mesh.nodes, id=fz.id) - for fz in fluid_mesh.face_zones - if "interior" not in fz.name - ] + # boundaries = [ + # SurfaceMesh(name=fz.name, triangles=fz.faces, nodes=fluid_mesh.nodes, id=fz.id) + # for fz in fluid_mesh.face_zones + # if "interior" not in fz.name + # ] - self.fluid_mesh = Mesh(fluid_mesh_vtk) - for boundary in boundaries: - self.fluid_mesh.add_surface(boundary, boundary.id, boundary.name) + self.fluid_mesh = Mesh(fluid_mesh) + # for boundary in boundaries: + # self.fluid_mesh.add_surface(boundary, boundary.id, boundary.name) return diff --git a/src/ansys/heart/core/pre/mesher.py b/src/ansys/heart/core/pre/mesher.py index da1b856fc..cf6d39902 100644 --- a/src/ansys/heart/core/pre/mesher.py +++ b/src/ansys/heart/core/pre/mesher.py @@ -26,7 +26,7 @@ import os from pathlib import Path import shutil -from typing import List, Union +from typing import Union import numpy as np import pyvista as pv @@ -467,29 +467,28 @@ def _set_size_field_on_face_zones( # TODO: fix method. def _mesh_fluid_cavities( - fluid_boundaries: List[SurfaceMesh], - caps: List[SurfaceMesh], - workdir: str, - remesh_caps: bool = True, -) -> _FluentMesh: - """Mesh the fluid cavities. + cavity_boundaries: list[SurfaceMesh], workdir: str, mesh_size: float = 1.0 +) -> pv.UnstructuredGrid: + """Mesh the caps of each fluid cavity with uniformly. Parameters ---------- - fluid_boundaries : List[SurfaceMesh] - List of fluid boundaries used for meshing. - caps : List[SurfaceMesh] - List of caps that close each of the cavities. + cavity_boundaries : List[SurfaceMesh] + List of cavity boundaries used for meshing. workdir : str Working directory - remesh_caps : bool, optional - Flag indicating whether to remesh the caps, by default True + mesh_size : float + Mesh size Returns ------- - Path - Path to the .msh.h5 volume mesh. + pv.UnstructuredGrid + Unstructured grid with fluid mesh. """ + # Use the following tgrid api utility for each cavity: + # Consequently need to associate each "patch" with a cap. E.g. by centroid? + # + # (tgapi-util-fill-holes-in-face-zone-list '(face-zone-list) max-hole-edges) if _uses_container: mounted_volume = pyfluent.EXAMPLES_PATH work_dir_meshing = os.path.join(mounted_volume, "tmp_meshing-fluid") @@ -504,16 +503,11 @@ def _mesh_fluid_cavities( os.remove(f) # write all boundaries - for b in fluid_boundaries: + for b in cavity_boundaries: filename = os.path.join(work_dir_meshing, b.name.lower() + ".stl") b.save(filename) add_solid_name_to_stl(filename, b.name.lower(), file_type="binary") - for c in caps: - filename = os.path.join(work_dir_meshing, c.name.lower() + ".stl") - c.save(filename) - add_solid_name_to_stl(filename, c.name.lower(), file_type="binary") - session = _get_fluent_meshing_session(work_dir_meshing) # import all stls @@ -522,43 +516,74 @@ def _mesh_fluid_cavities( # will be in /mnt/pyfluent. So need to use relative paths # or replace dirname by /mnt/pyfluent as prefix work_dir_meshing = "/mnt/pyfluent/meshing" - session.tui.file.import_.cad(f"no {work_dir_meshing} *.stl") - - # merge objects - session.tui.objects.merge("'(*)", "model-fluid") - # fix duplicate nodes - session.tui.diagnostics.face_connectivity.fix_free_faces("objects '(*)") + session.tui.file.import_.cad(f"no {work_dir_meshing} *.stl") # set size field - session.tui.size_functions.set_global_controls(1, 1, 1.2) + session.tui.size_functions.set_global_controls(mesh_size, mesh_size, 1.2) session.tui.scoped_sizing.compute("yes") - # remesh all caps - if remesh_caps: - session.tui.boundary.remesh.remesh_constant_size("(cap_*)", "()", 40, 20, 1, "yes") - - # convert to mesh object - session.tui.objects.change_object_type("(*)", "mesh", "yes") - - # compute volumetric regions - session.tui.objects.volumetric_regions.compute("model-fluid") + # create caps + # (tgapi-util-fill-holes-in-face-zone-list '(face-zone-list) max-hole-edges) + # convert all to mesh object + session.tui.objects.change_object_type("'(*)", "mesh", "yes") + for cavity_boundary in cavity_boundaries: + cavity_name = "-".join(cavity_boundary.name.split()).lower() + session.scheme_eval.scheme_eval( + f"(tgapi-util-fill-holes-in-face-zone-list '({cavity_name}) 1000)" + ) + # merge unreferenced with cavity + # (get-unreferenced-face-zones) + patch_ids = session.scheme_eval.scheme_eval("(get-unreferenced-face-zones)") + session.tui.objects.create( + f"{cavity_name}-patches", + "fluid", + 3, + "({0})".format(" ".join([str(patch_id) for patch_id in patch_ids])), + "()", + "mesh", + "yes", + ) + # merge objects + session.tui.objects.merge(f"'({cavity_name}*)") + + # find overlapping pairs + # (tgapi-util-get-overlapping-face-zones "*patch*" 0.1 0.1 ) + # try: + # overlapping_pairs = session.scheme_eval.scheme_eval( + # '(tgapi-util-get-overlapping-face-zones "*patch*" 0.1 0.1 )' + # ) + # for pair in overlapping_pairs: + # # remove first from pair + # session.tui.boundary.manage.delete(f"'({pair[0]})", "yes") + # except: + # LOGGER.debug("No overlapping face zones.") + + # merge mesh objects + mesh_object_names = session.scheme_eval.scheme_eval("(get-objects-of-type 'mesh)") + if len(mesh_object_names) > 1: + session.tui.objects.merge("'(*)", "fluid-zones") + else: + session.tui.objects.rename_object(mesh_object_names[0].str, "fluid-zones") - # mesh volume - session.tui.mesh.auto_mesh("model-fluid") + # merge nodes + session.tui.boundary.merge_nodes("'(*)", "'(*)", "no", "no , ,") + # mesh volumes + session.tui.objects.volumetric_regions.compute("fluid-zones", "no") + session.tui.mesh.auto_mesh("fluid-zones", "yes", "pyr", "tet", "yes") - # clean up session.tui.objects.delete_all_geom() - session.tui.objects.delete_unreferenced_faces_and_edges() - # write file_path_mesh = os.path.join(workdir, "fluid-mesh.msh.h5") session.tui.file.write_mesh(file_path_mesh) + session.exit() + + # # write mesh = _FluentMesh(file_path_mesh) - mesh.load_mesh() + mesh.load_mesh(reconstruct_tetrahedrons=True) - return mesh + return mesh._to_vtk(add_cells=True, add_faces=True, remove_interior_faces=True) def mesh_from_manifold_input_model( diff --git a/src/ansys/heart/core/utils/fluent_reader.py b/src/ansys/heart/core/utils/fluent_reader.py index 900677fbd..fdd868b7d 100644 --- a/src/ansys/heart/core/utils/fluent_reader.py +++ b/src/ansys/heart/core/utils/fluent_reader.py @@ -362,7 +362,9 @@ def _convert_interior_faces_to_tetrahedrons(self) -> Tuple[np.ndarray, np.ndarra return tetrahedrons, cell_ids # NOTE: no typehint due to lazy import of pyvista - def _to_vtk(self, add_cells: bool = True, add_faces: bool = False): + def _to_vtk( + self, add_cells: bool = True, add_faces: bool = False, remove_interior_faces: bool = False + ): """Convert mesh to vtk unstructured grid or polydata. Parameters @@ -371,6 +373,8 @@ def _to_vtk(self, add_cells: bool = True, add_faces: bool = False): Whether to add cells to the vtk object, by default True add_faces : bool, optional Whether to add faces to the vtk object, by default False + remove_interior_faces : bool, optional + Remove interior faces, by default False Returns ------- @@ -407,11 +411,16 @@ def _to_vtk(self, add_cells: bool = True, add_faces: bool = False): if add_faces: # add faces. + face_zones = self.face_zones + + if remove_interior_faces: + face_zones = [fz for fz in face_zones if "interior" not in fz.name] + grid_faces = pv.UnstructuredGrid() grid_faces.nodes = self.nodes - face_zone_ids = np.concatenate([[fz.id] * fz.faces.shape[0] for fz in self.face_zones]) - faces = np.array(np.concatenate([fz.faces for fz in self.face_zones]), dtype=int) + face_zone_ids = np.concatenate([[fz.id] * fz.faces.shape[0] for fz in face_zones]) + faces = np.array(np.concatenate([fz.faces for fz in face_zones]), dtype=int) faces = np.hstack([np.ones((faces.shape[0], 1), dtype=int) * 3, faces]) grid_faces = pv.UnstructuredGrid( From c8fa17d6f8eab595a2df79700cb5142b8de278bb Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:08:05 +0200 Subject: [PATCH 02/12] fix: fixes and cleanup --- src/ansys/heart/core/models.py | 52 +++++++++--------------------- src/ansys/heart/core/pre/mesher.py | 20 +----------- 2 files changed, 16 insertions(+), 56 deletions(-) diff --git a/src/ansys/heart/core/models.py b/src/ansys/heart/core/models.py index c3ea35efa..96371a378 100644 --- a/src/ansys/heart/core/models.py +++ b/src/ansys/heart/core/models.py @@ -520,9 +520,12 @@ def mesh_volume( return self.mesh + # TODO: simplify def _mesh_fluid_volume(self): """Generate a volume mesh of the cavities.""" # get all relevant boundaries for the fluid cavities: + + # TODO: use naming convention caps. substrings_include = ["endocardium", "valve-plane", "septum"] substrings_include_re = "|".join(substrings_include) @@ -537,14 +540,8 @@ def _mesh_fluid_volume(self): ] boundaries_fluid = [b for b in boundaries_fluid if b.name not in boundaries_exclude] - caps = [c._mesh for p in self.parts for c in p.caps] - if len(boundaries_fluid) == 0: - LOGGER.debug("Meshing of fluid cavities not possible. No fluid surfaces detected.") - return - - if len(caps) == 0: - LOGGER.debug("Meshing of fluid cavities not possible. No caps detected.") + LOGGER.error("Meshing of fluid cavities not possible. No fluid surfaces detected.") return LOGGER.info("Meshing fluid cavities...") @@ -554,40 +551,21 @@ def _mesh_fluid_volume(self): cavity_surfaces = [ self.mesh.get_surface(part.cavity.surface.id) for part in self.parts if part.cavity ] - # remove caps. - cavity_surfaces = [ - SurfaceMesh(cs.threshold((0, 0), "_cap_id").extract_surface(), name=cs.name) - for cs in cavity_surfaces - ] - fluid_mesh = mesher._mesh_fluid_cavities(cavity_surfaces, self.workdir, mesh_size=1) - - # LOGGER.info(f"Meshed {len(fluid_mesh.cell_zones)} fluid regions...") - # add part-ids - # cz_ids = np.sort([cz.id for cz in fluid_mesh.cell_zones]) - - # TODO: this offset is arbitrary. - # offset = 10000 - # new_ids = np.arange(cz_ids.shape[0]) + offset - # czid_to_pid = {cz_id: new_ids[ii] for ii, cz_id in enumerate(cz_ids)} - - # for cz in fluid_mesh.cell_zones: - # cz.id = czid_to_pid[cz.id] - - # fluid_mesh._fix_negative_cells() - # fluid_mesh_vtk = fluid_mesh._to_vtk(add_cells=True, add_faces=False) - - # fluid_mesh_vtk.cell_data["part-id"] = fluid_mesh_vtk.cell_data["cell-zone-ids"] + # get cap meshes. + # TODO: for right ventricle add septal surface. + cavity_surfaces = [] + for part in self.parts: + try: + cavity_surfaces += [self.mesh.get_surface(part.endocardium.id)] + except AttributeError: + pass - # boundaries = [ - # SurfaceMesh(name=fz.name, triangles=fz.faces, nodes=fluid_mesh.nodes, id=fz.id) - # for fz in fluid_mesh.face_zones - # if "interior" not in fz.name - # ] + fluid_mesh = mesher._mesh_fluid_cavities(cavity_surfaces, self.workdir, mesh_size=1) + # rename cell-zone-ids to part-ids + # TODO: check if all face zones properly exist. self.fluid_mesh = Mesh(fluid_mesh) - # for boundary in boundaries: - # self.fluid_mesh.add_surface(boundary, boundary.id, boundary.name) return diff --git a/src/ansys/heart/core/pre/mesher.py b/src/ansys/heart/core/pre/mesher.py index cf6d39902..cd088816f 100644 --- a/src/ansys/heart/core/pre/mesher.py +++ b/src/ansys/heart/core/pre/mesher.py @@ -485,10 +485,6 @@ def _mesh_fluid_cavities( pv.UnstructuredGrid Unstructured grid with fluid mesh. """ - # Use the following tgrid api utility for each cavity: - # Consequently need to associate each "patch" with a cap. E.g. by centroid? - # - # (tgapi-util-fill-holes-in-face-zone-list '(face-zone-list) max-hole-edges) if _uses_container: mounted_volume = pyfluent.EXAMPLES_PATH work_dir_meshing = os.path.join(mounted_volume, "tmp_meshing-fluid") @@ -523,9 +519,7 @@ def _mesh_fluid_cavities( session.tui.size_functions.set_global_controls(mesh_size, mesh_size, 1.2) session.tui.scoped_sizing.compute("yes") - # create caps - # (tgapi-util-fill-holes-in-face-zone-list '(face-zone-list) max-hole-edges) - # convert all to mesh object + # create caps with uniform size. session.tui.objects.change_object_type("'(*)", "mesh", "yes") for cavity_boundary in cavity_boundaries: cavity_name = "-".join(cavity_boundary.name.split()).lower() @@ -547,18 +541,6 @@ def _mesh_fluid_cavities( # merge objects session.tui.objects.merge(f"'({cavity_name}*)") - # find overlapping pairs - # (tgapi-util-get-overlapping-face-zones "*patch*" 0.1 0.1 ) - # try: - # overlapping_pairs = session.scheme_eval.scheme_eval( - # '(tgapi-util-get-overlapping-face-zones "*patch*" 0.1 0.1 )' - # ) - # for pair in overlapping_pairs: - # # remove first from pair - # session.tui.boundary.manage.delete(f"'({pair[0]})", "yes") - # except: - # LOGGER.debug("No overlapping face zones.") - # merge mesh objects mesh_object_names = session.scheme_eval.scheme_eval("(get-objects-of-type 'mesh)") if len(mesh_object_names) > 1: From b2e182b13bab95ecde28f05c49508996836d7b25 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 8 Apr 2025 08:38:16 +0000 Subject: [PATCH 03/12] chore: adding changelog file 1028.added.md [dependabot-skip] --- doc/source/changelog/1028.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/source/changelog/1028.added.md diff --git a/doc/source/changelog/1028.added.md b/doc/source/changelog/1028.added.md new file mode 100644 index 000000000..71aa3b01d --- /dev/null +++ b/doc/source/changelog/1028.added.md @@ -0,0 +1 @@ +mesh blood pools \ No newline at end of file From 720487c0d6c476c61af947612ccc26c79830ae73 Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:11:53 +0200 Subject: [PATCH 04/12] feat: add method to mesh from fluid boundaries --- src/ansys/heart/core/pre/mesher.py | 97 ++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/ansys/heart/core/pre/mesher.py b/src/ansys/heart/core/pre/mesher.py index cd088816f..96952c696 100644 --- a/src/ansys/heart/core/pre/mesher.py +++ b/src/ansys/heart/core/pre/mesher.py @@ -1048,3 +1048,100 @@ def mesh_from_non_manifold_input_model( vtk_mesh = _post_meshing_cleanup(new_mesh) return vtk_mesh + + +def _mesh_fluid_from_boundaries( + fluid_boundaries: list[SurfaceMesh], + workdir: str, + mesh_size: float = 1.0, +) -> pv.UnstructuredGrid: + """Mesh the fluid from the boundary surfaces. + + Parameters + ---------- + fluid_boundaries : List[SurfaceMesh] + List of fluid boundaries used for meshing. + workdir : str + Working directory + mesh_size : float + Mesh size of the patches that. + + Returns + ------- + pv.UnstructuredGrid + Unstructured grid with fluid mesh. + """ + if _uses_container: + mounted_volume = pyfluent.EXAMPLES_PATH + work_dir_meshing = os.path.join(mounted_volume, "tmp_meshing-fluid") + else: + work_dir_meshing = os.path.join(workdir, "meshing-fluid") + + if not os.path.isdir(work_dir_meshing): + os.makedirs(work_dir_meshing) + else: + files = glob.glob(os.path.join(work_dir_meshing, "*.stl")) + for f in files: + os.remove(f) + + # write all boundaries + for b in fluid_boundaries: + filename = os.path.join(work_dir_meshing, b.name.lower() + ".stl") + b.save(filename) + add_solid_name_to_stl(filename, b.name.lower(), file_type="binary") + + session = _get_fluent_meshing_session(work_dir_meshing) + + # import all stls + if _uses_container: + # NOTE: when using a Fluent container visible files + # will be in /mnt/pyfluent. So need to use relative paths + # or replace dirname by /mnt/pyfluent as prefix + work_dir_meshing = "/mnt/pyfluent/meshing" + + session.tui.file.import_.cad(f"no {work_dir_meshing} *.stl") + + # set size field + session.tui.size_functions.set_global_controls(mesh_size, mesh_size, 1.2) + session.tui.scoped_sizing.compute("yes") + + # create caps with uniform size. + session.tui.objects.merge("(*)", "fluid-mesh") + # object_names = list(session.scheme_eval.scheme_eval("(tgapi-util-get-all-object-name-list)")) + # session.tui.objects.rename_object(object_names[0], "fluid-mesh") + session.tui.diagnostics.face_connectivity.fix_free_faces( + "objects '(fluid-mesh) merge-nodes yes 1e-3" + ) + session.tui.objects.change_object_type("'(fluid-mesh)", "mesh", "yes") + + session.scheme_eval.scheme_eval("(tgapi-util-fill-holes-in-face-zone-list '(*) 1000)") + + patch_ids = session.scheme_eval.scheme_eval("(get-unreferenced-face-zones)") + + session.tui.objects.create( + "mesh-patches", + "fluid", + 3, + "({0})".format(" ".join([str(patch_id) for patch_id in patch_ids])), + "()", + "mesh", + "yes", + ) + session.tui.objects.merge("'(*)") + + # compute volume and mesh + session.tui.objects.volumetric_regions.compute("fluid-mesh", "no") + session.tui.mesh.auto_mesh("fluid-mesh", "yes", "pyr", "tet", "yes") + + session.tui.objects.delete_all_geom() + + file_path_mesh = os.path.join(workdir, "fluid-mesh.msh.h5") + session.tui.file.write_mesh(file_path_mesh) + + session.exit() + + # write to file. + mesh = _FluentMesh(file_path_mesh) + mesh.load_mesh(reconstruct_tetrahedrons=True) + + return mesh._to_vtk(add_cells=True, add_faces=True, remove_interior_faces=True) From a4c614d734b9ba976917ff6e8bda8f024ab2e052 Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:12:32 +0200 Subject: [PATCH 05/12] feat: cleanup --- src/ansys/heart/core/models.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ansys/heart/core/models.py b/src/ansys/heart/core/models.py index 96371a378..89a093121 100644 --- a/src/ansys/heart/core/models.py +++ b/src/ansys/heart/core/models.py @@ -527,16 +527,16 @@ def _mesh_fluid_volume(self): # TODO: use naming convention caps. substrings_include = ["endocardium", "valve-plane", "septum"] - substrings_include_re = "|".join(substrings_include) + substrings_include_regex = "|".join(substrings_include) - substrings_exlude = ["pulmonary-valve", "aortic-valve"] - substrings_exlude_re = "|".join(substrings_exlude) + substrings_exlude = [CapType.PULMONARY_VALVE.value, CapType.AORTIC_VALVE.value] + substrings_exlude_regex = "|".join(substrings_exlude) boundaries_fluid = [ - b for b in self.mesh._surfaces if re.search(substrings_include_re, b.name) + b for b in self.mesh._surfaces if re.search(substrings_include_regex, b.name) ] boundaries_exclude = [ - b.name for b in boundaries_fluid if re.search(substrings_exlude_re, b.name) + b.name for b in boundaries_fluid if re.search(substrings_exlude_regex, b.name) ] boundaries_fluid = [b for b in boundaries_fluid if b.name not in boundaries_exclude] @@ -561,7 +561,11 @@ def _mesh_fluid_volume(self): except AttributeError: pass - fluid_mesh = mesher._mesh_fluid_cavities(cavity_surfaces, self.workdir, mesh_size=1) + fluid_mesh = mesher._mesh_fluid_from_boundaries( + cavity_surfaces, boundaries_fluid, self.workdir, mesh_size=1 + ) + + # TODO: rename caps accordingly # rename cell-zone-ids to part-ids # TODO: check if all face zones properly exist. From dba3a9b2677d50e6e214f52b52be821dabee8076 Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:13:49 +0200 Subject: [PATCH 06/12] feat: cleanup --- src/ansys/heart/core/models.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/ansys/heart/core/models.py b/src/ansys/heart/core/models.py index 89a093121..6ace727da 100644 --- a/src/ansys/heart/core/models.py +++ b/src/ansys/heart/core/models.py @@ -541,29 +541,12 @@ def _mesh_fluid_volume(self): boundaries_fluid = [b for b in boundaries_fluid if b.name not in boundaries_exclude] if len(boundaries_fluid) == 0: - LOGGER.error("Meshing of fluid cavities not possible. No fluid surfaces detected.") + LOGGER.error("Meshing of blood pool not possible. No fluid surfaces detected.") return - LOGGER.info("Meshing fluid cavities...") + LOGGER.info("Meshing blood pool...") - # get list of fluid cavities - # mesh the fluid cavities - cavity_surfaces = [ - self.mesh.get_surface(part.cavity.surface.id) for part in self.parts if part.cavity - ] - - # get cap meshes. - # TODO: for right ventricle add septal surface. - cavity_surfaces = [] - for part in self.parts: - try: - cavity_surfaces += [self.mesh.get_surface(part.endocardium.id)] - except AttributeError: - pass - - fluid_mesh = mesher._mesh_fluid_from_boundaries( - cavity_surfaces, boundaries_fluid, self.workdir, mesh_size=1 - ) + fluid_mesh = mesher._mesh_fluid_from_boundaries(boundaries_fluid, self.workdir, mesh_size=1) # TODO: rename caps accordingly From e25b61235e566a1c725a25cf8aeb4e44cf1dc24d Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:14:05 +0200 Subject: [PATCH 07/12] feat: to non public method --- src/ansys/heart/core/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ansys/heart/core/models.py b/src/ansys/heart/core/models.py index 6ace727da..9d2b18d46 100644 --- a/src/ansys/heart/core/models.py +++ b/src/ansys/heart/core/models.py @@ -291,7 +291,7 @@ def __init__(self, working_directory: pathlib.Path | str = None) -> None: self.mesh = Mesh() """Computational mesh.""" - self.fluid_mesh = Mesh() + self._fluid_mesh = Mesh() """Generated fluid mesh.""" #! TODO: non-functional flag. Remove or replace. @@ -552,7 +552,7 @@ def _mesh_fluid_volume(self): # rename cell-zone-ids to part-ids # TODO: check if all face zones properly exist. - self.fluid_mesh = Mesh(fluid_mesh) + self._fluid_mesh = Mesh(fluid_mesh) return From b34df3d3861861d691800e0da484b90c8c931ce1 Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Tue, 8 Apr 2025 13:16:23 +0200 Subject: [PATCH 08/12] feat: cleanup old method --- src/ansys/heart/core/pre/mesher.py | 103 ----------------------------- 1 file changed, 103 deletions(-) diff --git a/src/ansys/heart/core/pre/mesher.py b/src/ansys/heart/core/pre/mesher.py index 96952c696..bb725566a 100644 --- a/src/ansys/heart/core/pre/mesher.py +++ b/src/ansys/heart/core/pre/mesher.py @@ -465,109 +465,6 @@ def _set_size_field_on_face_zones( return session -# TODO: fix method. -def _mesh_fluid_cavities( - cavity_boundaries: list[SurfaceMesh], workdir: str, mesh_size: float = 1.0 -) -> pv.UnstructuredGrid: - """Mesh the caps of each fluid cavity with uniformly. - - Parameters - ---------- - cavity_boundaries : List[SurfaceMesh] - List of cavity boundaries used for meshing. - workdir : str - Working directory - mesh_size : float - Mesh size - - Returns - ------- - pv.UnstructuredGrid - Unstructured grid with fluid mesh. - """ - if _uses_container: - mounted_volume = pyfluent.EXAMPLES_PATH - work_dir_meshing = os.path.join(mounted_volume, "tmp_meshing-fluid") - else: - work_dir_meshing = os.path.join(workdir, "meshing-fluid") - - if not os.path.isdir(work_dir_meshing): - os.makedirs(work_dir_meshing) - else: - files = glob.glob(os.path.join(work_dir_meshing, "*.stl")) - for f in files: - os.remove(f) - - # write all boundaries - for b in cavity_boundaries: - filename = os.path.join(work_dir_meshing, b.name.lower() + ".stl") - b.save(filename) - add_solid_name_to_stl(filename, b.name.lower(), file_type="binary") - - session = _get_fluent_meshing_session(work_dir_meshing) - - # import all stls - if _uses_container: - # NOTE: when using a Fluent container visible files - # will be in /mnt/pyfluent. So need to use relative paths - # or replace dirname by /mnt/pyfluent as prefix - work_dir_meshing = "/mnt/pyfluent/meshing" - - session.tui.file.import_.cad(f"no {work_dir_meshing} *.stl") - - # set size field - session.tui.size_functions.set_global_controls(mesh_size, mesh_size, 1.2) - session.tui.scoped_sizing.compute("yes") - - # create caps with uniform size. - session.tui.objects.change_object_type("'(*)", "mesh", "yes") - for cavity_boundary in cavity_boundaries: - cavity_name = "-".join(cavity_boundary.name.split()).lower() - session.scheme_eval.scheme_eval( - f"(tgapi-util-fill-holes-in-face-zone-list '({cavity_name}) 1000)" - ) - # merge unreferenced with cavity - # (get-unreferenced-face-zones) - patch_ids = session.scheme_eval.scheme_eval("(get-unreferenced-face-zones)") - session.tui.objects.create( - f"{cavity_name}-patches", - "fluid", - 3, - "({0})".format(" ".join([str(patch_id) for patch_id in patch_ids])), - "()", - "mesh", - "yes", - ) - # merge objects - session.tui.objects.merge(f"'({cavity_name}*)") - - # merge mesh objects - mesh_object_names = session.scheme_eval.scheme_eval("(get-objects-of-type 'mesh)") - if len(mesh_object_names) > 1: - session.tui.objects.merge("'(*)", "fluid-zones") - else: - session.tui.objects.rename_object(mesh_object_names[0].str, "fluid-zones") - - # merge nodes - session.tui.boundary.merge_nodes("'(*)", "'(*)", "no", "no , ,") - # mesh volumes - session.tui.objects.volumetric_regions.compute("fluid-zones", "no") - session.tui.mesh.auto_mesh("fluid-zones", "yes", "pyr", "tet", "yes") - - session.tui.objects.delete_all_geom() - - file_path_mesh = os.path.join(workdir, "fluid-mesh.msh.h5") - session.tui.file.write_mesh(file_path_mesh) - - session.exit() - - # # write - mesh = _FluentMesh(file_path_mesh) - mesh.load_mesh(reconstruct_tetrahedrons=True) - - return mesh._to_vtk(add_cells=True, add_faces=True, remove_interior_faces=True) - - def mesh_from_manifold_input_model( model: _InputModel, workdir: Union[str, Path], From d4962cd61e2149db8737b503342c9717d7f01cec Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:34:40 +0200 Subject: [PATCH 09/12] feat: return Mesh and update cap names --- src/ansys/heart/core/models.py | 25 +++++++++++++++---------- src/ansys/heart/core/pre/mesher.py | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/ansys/heart/core/models.py b/src/ansys/heart/core/models.py index 9d2b18d46..7c9d923f3 100644 --- a/src/ansys/heart/core/models.py +++ b/src/ansys/heart/core/models.py @@ -520,12 +520,10 @@ def mesh_volume( return self.mesh - # TODO: simplify - def _mesh_fluid_volume(self): + def _mesh_fluid_volume(self) -> Mesh: """Generate a volume mesh of the cavities.""" - # get all relevant boundaries for the fluid cavities: - - # TODO: use naming convention caps. + # get all relevant boundaries for the fluid: + # NOTE: relies on substrings to select the right surfaces/boundaries. substrings_include = ["endocardium", "valve-plane", "septum"] substrings_include_regex = "|".join(substrings_include) @@ -548,13 +546,20 @@ def _mesh_fluid_volume(self): fluid_mesh = mesher._mesh_fluid_from_boundaries(boundaries_fluid, self.workdir, mesh_size=1) - # TODO: rename caps accordingly + # update patches with appropriate cap name, based on centroid location. + model_caps = [c for part in self.parts for c in part.caps] + cap_centroids = np.array([cap.centroid for cap in model_caps]) + cap_names = [cap.name for cap in model_caps] - # rename cell-zone-ids to part-ids - # TODO: check if all face zones properly exist. - self._fluid_mesh = Mesh(fluid_mesh) + patches = {sid: sn for sid, sn in fluid_mesh._surface_id_to_name.items() if "patch" in sn} + for patch_id in patches.keys(): + patch_mesh = fluid_mesh.get_surface(patch_id) + cap_index = np.argmin(np.linalg.norm(cap_centroids - patch_mesh.center, axis=1)) + fluid_mesh._surface_id_to_name[patch_id] = cap_names[cap_index] - return + self._fluid_mesh = fluid_mesh + + return fluid_mesh def get_part(self, name: str, by_substring: bool = False) -> Union[Part, None]: """Get specific part based on part name.""" diff --git a/src/ansys/heart/core/pre/mesher.py b/src/ansys/heart/core/pre/mesher.py index bb725566a..6929291d2 100644 --- a/src/ansys/heart/core/pre/mesher.py +++ b/src/ansys/heart/core/pre/mesher.py @@ -951,7 +951,7 @@ def _mesh_fluid_from_boundaries( fluid_boundaries: list[SurfaceMesh], workdir: str, mesh_size: float = 1.0, -) -> pv.UnstructuredGrid: +) -> Mesh: """Mesh the fluid from the boundary surfaces. Parameters @@ -1033,7 +1033,9 @@ def _mesh_fluid_from_boundaries( session.tui.objects.delete_all_geom() file_path_mesh = os.path.join(workdir, "fluid-mesh.msh.h5") - session.tui.file.write_mesh(file_path_mesh) + if os.path.isfile(file_path_mesh): + os.remove(file_path_mesh) + session.tui.file.write_mesh(file_path_mesh, "ok") session.exit() @@ -1041,4 +1043,15 @@ def _mesh_fluid_from_boundaries( mesh = _FluentMesh(file_path_mesh) mesh.load_mesh(reconstruct_tetrahedrons=True) - return mesh._to_vtk(add_cells=True, add_faces=True, remove_interior_faces=True) + vtk_mesh = Mesh(mesh._to_vtk(add_cells=True, add_faces=True, remove_interior_faces=True)) + vtk_mesh.rename_array("face-zone-ids", "_surface-id") + vtk_mesh.rename_array("cell-zone-ids", "_volume-id") + + vtk_mesh._surface_id_to_name = { + int(fz.id): fz.name for fz in mesh.face_zones if fz.id in vtk_mesh.surface_ids + } + vtk_mesh._volume_id_to_name = { + int(cz.id): cz.name for cz in mesh.cell_zones if cz.id in vtk_mesh.volume_ids + } + + return vtk_mesh From 450cbd3026ca8060acce30b2c792131681d80798 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:02:59 +0000 Subject: [PATCH 10/12] chore: adding changelog file 1028.added.md [dependabot-skip] --- doc/source/changelog/1028.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/changelog/1028.added.md b/doc/source/changelog/1028.added.md index 71aa3b01d..25d7c395f 100644 --- a/doc/source/changelog/1028.added.md +++ b/doc/source/changelog/1028.added.md @@ -1 +1 @@ -mesh blood pools \ No newline at end of file +Mesh blood pools \ No newline at end of file From f6d5c73b9eefda4533afd7d059351ac41bb56f1f Mon Sep 17 00:00:00 2001 From: mhoeijm <102799582+mhoeijm@users.noreply.github.com> Date: Mon, 11 Aug 2025 13:01:08 +0200 Subject: [PATCH 11/12] do not allow CONTAINER and PIM modes --- src/ansys/health/heart/pre/mesher.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ansys/health/heart/pre/mesher.py b/src/ansys/health/heart/pre/mesher.py index 892ef5237..86e24b9f4 100644 --- a/src/ansys/health/heart/pre/mesher.py +++ b/src/ansys/health/heart/pre/mesher.py @@ -1042,6 +1042,12 @@ def _mesh_fluid_from_boundaries( pv.UnstructuredGrid Unstructured grid with fluid mesh. """ + if _launch_mode in [LaunchMode.CONTAINER, LaunchMode.PIM]: + raise NotImplementedError( + "Meshing of fluid boundaries is not yet supported in PIM mode. " + "Please use the containerized or standalone mode." + ) + if _uses_container: mounted_volume = pyfluent.EXAMPLES_PATH work_dir_meshing = os.path.join(mounted_volume, "tmp_meshing-fluid") @@ -1063,11 +1069,20 @@ def _mesh_fluid_from_boundaries( session = _get_fluent_meshing_session(work_dir_meshing) - # import all stls - if _uses_container: + LOGGER.info(f"Starting Fluent Meshing in mode: {_launch_mode}") + + if _launch_mode == LaunchMode.PIM: + # Upload files to session if in PIM or Container modes. + LOGGER.info(f"Uploading files to session with working directory {work_dir_meshing}...") + files = glob.glob(os.path.join(work_dir_meshing, "*.stl")) + for file in files: + session.upload(file) + # In PIM mode files are uploaded to the Fluents working directory. + work_dir_meshing = "." + + elif _launch_mode == LaunchMode.CONTAINER: # NOTE: when using a Fluent container visible files - # will be in /mnt/pyfluent. So need to use relative paths - # or replace dirname by /mnt/pyfluent as prefix + # will be in /mnt/pyfluent. (equal to mount target) work_dir_meshing = "/mnt/pyfluent/meshing" session.tui.file.import_.cad(f"no {work_dir_meshing} *.stl") From be01d28761d9e475f916225bf9deb2f2f0351154 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 19 Aug 2025 08:16:16 +0000 Subject: [PATCH 12/12] chore: adding changelog file 1028.added.md [dependabot-skip] --- doc/source/changelog/1028.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/changelog/1028.added.md b/doc/source/changelog/1028.added.md index 25d7c395f..8d63d46e9 100644 --- a/doc/source/changelog/1028.added.md +++ b/doc/source/changelog/1028.added.md @@ -1 +1 @@ -Mesh blood pools \ No newline at end of file +Mesh blood pools