Source code for vasp.automatedPreprocessing.preprocessing_common

# Copyright (c) 2023 Simula Research Laboratory
# SPDX-License-Identifier: GPL-3.0-or-later

from pathlib import Path
from typing import Union

import h5py
import numpy as np
import meshio
from dolfin import Mesh, MeshFunction, File, HDF5File, FunctionSpace, Function, XDMFFile, cells, Edge
from vmtk import vmtkdistancetospheres, vmtkdijkstradistancetopoints
from morphman import vmtkscripts, write_polydata

from vasp.automatedPreprocessing.vmtkmeshgeneratorfsi import vmtkMeshGeneratorFsi
from vasp.simulations.simulation_common import load_mesh_and_data

from vtk import vtkPolyData

# Global array names
distanceToSpheresArrayName = "DistanceToSpheres"
distanceToSpheresArrayNameSolid = "Thickness"
dijkstraArrayName = "DijkstraDistanceToPoints"


[docs] def distance_to_spheres_solid_thickness(surface: vtkPolyData, save_path: Union[str, Path], distance_offset: float = 0, distance_scale: float = 0.1, min_distance: float = 0.25, max_distance: float = 0.3) -> vtkPolyData: """ Determines the solid thickness using vmtkdistancetospheres. Write the distance data to `save_path`. Args: surface (vtkPolyData): Input surface model save_path (Union[str, Path]): Location to store processed surface distance_offset (float): Offset added to the distances distance_scale (float): Scale applied to the distances min_distance (float): Minimum value for the distances max_distance (float): Maximum value for the distances Returns: surface (vtkPolyData): Processed surface model with info on solid thickness """ distanceToSpheres = vmtkdistancetospheres.vmtkDistanceToSpheres() distanceToSpheres.Surface = surface distanceToSpheres.DistanceOffset = distance_offset distanceToSpheres.DistanceScale = distance_scale distanceToSpheres.MinDistance = min_distance distanceToSpheres.MaxDistance = max_distance distanceToSpheres.DistanceToSpheresArrayName = distanceToSpheresArrayNameSolid distanceToSpheres.Execute() distance_to_sphere = distanceToSpheres.Surface write_polydata(distance_to_sphere, str(save_path)) return distance_to_sphere
[docs] def dist_sphere_spheres(surface: vtkPolyData, save_path: Union[str, Path], distance_offset: float, distance_scale: float, min_distance: float, max_distance: float, distance_method: str = 'geodesic') -> vtkPolyData: """ Determines the target edge length for each cell on the surface, including potential refinement or coarsening of certain user specified areas. Level of refinement/coarseness is determined based on the distance to the spheres. The distance computation can be either 'euclidean' or 'geodesic' (default). Args: surface (vtkPolyData): Input surface model save_path (Union[str, Path]): Location to store processed surface distance_offset (float): Offset added to the distances distance_scale (float): Scale applied to the distances min_distance (float): Minimum value for the distances max_distance (float): Maximum value for the distances distance_method (str): Method to compute distances ('euclidean' or 'geodesic') Returns: surface (vtkPolyData): Processed surface model with info on cell specific target edge length """ if distance_method == 'euclidean': distanceToSpheres = vmtkdistancetospheres.vmtkDistanceToSpheres() distance_array_name = distanceToSpheresArrayName elif distance_method == 'geodesic': distanceToSpheres = vmtkdijkstradistancetopoints.vmtkDijkstraDistanceToPoints() distance_array_name = dijkstraArrayName else: raise ValueError("Invalid distance computation method. Choose 'euclidean' or 'geodesic'.") distanceToSpheres.Surface = surface distanceToSpheres.DistanceOffset = distance_offset distanceToSpheres.DistanceScale = distance_scale distanceToSpheres.MinDistance = min_distance distanceToSpheres.MaxDistance = max_distance distanceToSpheres.Execute() distance_to_sphere = distanceToSpheres.Surface surfaceCurvature = vmtkscripts.vmtkSurfaceCurvature() surfaceCurvature.AbsoluteCurvature = 1 surfaceCurvature.MedianFiltering = 1 surfaceCurvature.CurvatureType = "gaussian" surfaceCurvature.Offset = 0.15 surfaceCurvature.BoundedReciprocal = 1 surfaceCurvature.Surface = distance_to_sphere surfaceCurvature.Execute() distance_to_sphere = surfaceCurvature.Surface surfaceArrayOperation = vmtkscripts.vmtkSurfaceArrayOperation() surfaceArrayOperation.Surface = distance_to_sphere surfaceArrayOperation.InputArrayName = "Curvature" surfaceArrayOperation.Input2ArrayName = distance_array_name surfaceArrayOperation.ResultArrayName = "Size" surfaceArrayOperation.Operation = "multiply" surfaceArrayOperation.Execute() distance_to_sphere = surfaceArrayOperation.Surface write_polydata(distance_to_sphere, str(save_path)) return distance_to_sphere
[docs] def generate_mesh(surface: vtkPolyData, number_of_sublayers_fluid: int, number_of_sublayers_solid: int, solid_thickness: str, solid_thickness_parameters: list, centerlines: vtkPolyData, solid_side_wall_id: int = 11, interface_fsi_id: int = 22, solid_outer_wall_id: int = 33, fluid_volume_id: int = 0, solid_volume_id: int = 1, no_solid: bool = False, extract_branch: bool = False, branch_group_ids: list = [], branch_ids_offset: int = 1000) -> tuple: """ Generates a mesh suitable for FSI from an input surface model. Args: surface (vtkPolyData): Surface model to be meshed. number_of_sublayers_fluid (int): Number of sublayers for fluid. number_of_sublayers_solid (int): Number of sublayers for solid. solid_thickness (str): Type of solid thickness ('variable' or 'constant'). solid_thickness_parameters (list): List of parameters for solid thickness. centerlines (vtkPolyData): Centerlines of input model. solid_side_wall_id (int, optional): ID for solid side wall. Default is 11. interface_fsi_id (int, optional): ID for the FSI interface. Default is 22. solid_outer_wall_id (int, optional): ID for solid outer wall. Default is 33. fluid_volume_id (int, optional): ID for the fluid volume. Default is 0. solid_volume_id (int, optional): ID for the solid volume. Default is 1. no_solid (bool, optional): Generate mesh without solid. extract_branch (bool, optional): Enable extraction of a specific branch, marking solid mesh IDs with an offset. branch_group_ids (list, optional): Specify group IDs to extract for the branch. branch_ids_offset (int): Set offset for marking solid mesh IDs when extracting a branch. Returns: tuple: A tuple containing the generated mesh (vtkUnstructuredGrid) and the remeshed surface (vtkPolyData). """ meshGenerator = vmtkscripts.vmtkMeshGenerator() if no_solid else vmtkMeshGeneratorFsi() meshGenerator.Surface = surface # Mesh Parameters meshGenerator.ElementSizeMode = "edgelengtharray" # Variable size mesh meshGenerator.TargetEdgeLengthArrayName = "Size" # Variable size mesh meshGenerator.LogOn = 1 meshGenerator.ExitOnError = 0 meshGenerator.BoundaryLayer = 1 meshGenerator.NumberOfSubLayersSolid = number_of_sublayers_solid meshGenerator.NumberOfSubLayersFluid = number_of_sublayers_fluid meshGenerator.NumberOfSubLayers = number_of_sublayers_fluid meshGenerator.BoundaryLayerOnCaps = 0 meshGenerator.SubLayerRatioFluid = 0.75 meshGenerator.SubLayerRatioSolid = 0.75 meshGenerator.SubLayerRatio = 0.75 meshGenerator.BoundaryLayerThicknessFactor = 0.5 meshGenerator.Tetrahedralize = 1 meshGenerator.VolumeElementScaleFactor = 0.8 meshGenerator.EndcapsEdgeLengthFactor = 1.0 meshGenerator.Centerlines = centerlines meshGenerator.ExtractBranch = extract_branch meshGenerator.BranchGroupIds = branch_group_ids meshGenerator.BranchIdsOffset = branch_ids_offset # Solid thickness handling if solid_thickness in ["variable", "painted"]: meshGenerator.ElementSizeModeSolid = "edgelengtharray" meshGenerator.TargetEdgeLengthArrayNameSolid = distanceToSpheresArrayNameSolid else: meshGenerator.ElementSizeModeSolid = "edgelength" meshGenerator.SolidThickness = solid_thickness_parameters[0] # IDs meshGenerator.SolidSideWallId = solid_side_wall_id meshGenerator.InterfaceFsiId = interface_fsi_id meshGenerator.SolidOuterWallId = solid_outer_wall_id meshGenerator.FluidVolumeId = fluid_volume_id meshGenerator.SolidVolumeId = solid_volume_id # Generate mesh meshGenerator.Execute() remeshed_surface = meshGenerator.RemeshedSurface generated_mesh = meshGenerator.Mesh return generated_mesh, remeshed_surface
[docs] def convert_xml_mesh_to_hdf5(file_name_xml_mesh: Union[str, Path], scaling_factor: float = 1) -> None: """Converts an XML mesh to an HDF5 mesh. Args: file_name_xml_mesh (Union[str, Path]): The name of the XML mesh file. scaling_factor (float, optional): A scaling factor to apply to the mesh coordinates. The default value is 1 (no scaling). Note that probes and parameters inside _info.json file will not be scaled if you only scale HDF5 file. Returns: None Raises: FileNotFoundError: If the XML mesh file does not exist. """ # Check if the XML mesh file exists xml_mesh_path = Path(file_name_xml_mesh) if not xml_mesh_path.is_file(): raise FileNotFoundError(f"The file '{xml_mesh_path}' does not exist.") mesh = Mesh(str(xml_mesh_path)) # Rescale the mesh coordinates x = mesh.coordinates() x[:, :] *= scaling_factor mesh.bounding_box_tree().build(mesh) # Convert subdomains to mesh function boundaries = MeshFunction("size_t", mesh, 2, mesh.domains()) boundaries.set_values(boundaries.array() + 1) # FIXME: Explain why this is necessary base, first_dot, rest = xml_mesh_path.name.partition('.') file_name_boundaries = str(xml_mesh_path.with_name(base + "_boundaries.pvd")) boundary_file = File(file_name_boundaries) boundary_file << boundaries domains = MeshFunction("size_t", mesh, 3, mesh.domains()) domains.set_values(domains.array() + 1) # in order to have fluid==1 and solid==2 file_name_domains = str(xml_mesh_path.with_name(base + "_domains.pvd")) domain_file = File(file_name_domains) domain_file << domains file_name_h5_mesh = str(xml_mesh_path.with_name(base + '.h5')) hdf = HDF5File(mesh.mpi_comm(), file_name_h5_mesh, "w") hdf.write(mesh, "/mesh") hdf.write(boundaries, "/boundaries") hdf.write(domains, "/domains")
[docs] def convert_vtu_mesh_to_xdmf(file_name_vtu_mesh: Union[str, Path], file_name_xdmf_mesh: Union[str, Path]) -> None: """ Convert a VTU mesh to XDMF format using meshio. This function is intended to run in serial. Args: file_name_vtu_mesh (Union[str, Path]): Path to the input VTU mesh file. file_name_xdmf_mesh (Union[str, Path]): Path to the output XDMF file. """ # Load the VTU mesh vtu_mesh = meshio.read(file_name_vtu_mesh) # Extract cell data tetra_data = vtu_mesh.cell_data_dict.get("CellEntityIds", {}).get("tetra", None) triangle_data = vtu_mesh.cell_data_dict.get("CellEntityIds", {}).get("triangle", None) # Extract cell types and data tetra_cells = None triangle_cells = None for cell in vtu_mesh.cells: if cell.type == "tetra": tetra_cells = cell.data elif cell.type == "triangle": triangle_cells = cell.data # Create mesh objects tetra_mesh = meshio.Mesh(points=vtu_mesh.points, cells={"tetra": tetra_cells}, cell_data={"CellEntityIds": [tetra_data]}) triangle_mesh = meshio.Mesh(points=vtu_mesh.points, cells=[("triangle", triangle_cells)], cell_data={"CellEntityIds": [triangle_data]}) # Define Path objects tetra_xdmf_path = Path(file_name_xdmf_mesh) triangle_xdmf_path = tetra_xdmf_path.with_name(tetra_xdmf_path.stem + '_triangle.xdmf') # Write the VTU mesh to XDMF format meshio.write(tetra_xdmf_path, tetra_mesh) meshio.write(triangle_xdmf_path, triangle_mesh) print(f"Tetra mesh XDMF file written to: {tetra_xdmf_path}") print(f"Triangle mesh XDMF file written to: {triangle_xdmf_path}\n")
[docs] def edge_length_evaluator(file_name_mesh: Union[str, Path], file_name_edge_length_xdmf: Union[str, Path]) -> None: """ Evaluates the edge length of a mesh. Args: file_name_mesh (Union[str, Path]): Path to the XML mesh file. file_name_edge_length_xdmf (Union[str, Path]): Path to the output XDMF file. """ file_name_mesh = Path(file_name_mesh) # Check if the XML mesh file exists if not file_name_mesh.is_file(): raise FileNotFoundError(f"The file '{file_name_mesh}' does not exist.") try: mesh = Mesh(str(file_name_mesh)) except RuntimeError: mesh = Mesh() with XDMFFile(str(file_name_mesh)) as xdmf: xdmf.read(mesh) mesh.init(1) num_cells = mesh.num_cells() V = FunctionSpace(mesh, "DG", 0) u = Function(V) values = np.zeros(num_cells, dtype=np.float64) for cell in cells(mesh): edges = cell.entities(1) value = 0 for edge in edges: value += Edge(mesh, edge).length() values[cell.index()] = value / len(edges) u.vector().set_local(values) u.vector().apply("local") u.rename("edge_length", "edge_length") with XDMFFile(str(file_name_edge_length_xdmf)) as xdmf: xdmf.write_checkpoint(u, "edge_length", 0, append=False)
[docs] def check_flatten_boundary(num_inlets_outlets: int, mesh_path: Union[str, Path], threshold_stdev: float = 0.001) -> None: """ Check whether inlets and outlets are flat, then flatten them if necessary Args: num_inlets_outlets (int): Number of inlets and outlets in the mesh mesh_path (Union[str, Path]): Path to the mesh file threshold_stdev (float): Threshold for standard deviation of facet unit normals Returns: None """ mesh_path = Path(mesh_path) flat_in_out_mesh_path = Path(str(mesh_path).replace(".h5", "_flat_outlet.h5")) # copy the mesh to a new file flat_in_out_mesh_path.write_bytes(mesh_path.read_bytes()) vectorData = h5py.File(str(flat_in_out_mesh_path), "a") facet_ids = np.array(vectorData["boundaries/values"]) facet_topology = vectorData["boundaries/topology"] fix = False for inlet_id in range(2, 2 + num_inlets_outlets): inlet_facet_ids = [i for i, x in enumerate(facet_ids) if x == inlet_id] inlet_facet_topology = facet_topology[inlet_facet_ids, :] inlet_nodes = np.unique(inlet_facet_topology.flatten()) # pre-allocate arrays inlet_facet_normals = np.zeros((len(inlet_facet_ids), 3)) # From: https://stackoverflow.com/questions/53698635/ # how-to-define-a-plane-with-3-points-and-plot-it-in-3d for idx, facet in enumerate(inlet_facet_topology): p0 = vectorData["boundaries/coordinates"][facet[0]] p1 = vectorData["boundaries/coordinates"][facet[1]] p2 = vectorData["boundaries/coordinates"][facet[2]] x0, y0, z0 = p0 x1, y1, z1 = p1 x2, y2, z2 = p2 ux, uy, uz = [x1 - x0, y1 - y0, z1 - z0] # Vectors vx, vy, vz = [x2 - x0, y2 - y0, z2 - z0] # cross product of vectors defines the plane normal u_cross_v = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx] normal = np.array(u_cross_v) # Facet unit normal vector (u_normal) u_normal = normal / np.sqrt( normal[0] ** 2 + normal[1] ** 2 + normal[2] ** 2 ) # check if facet unit normal vector has opposite # direction and reverse the vector if necessary if idx == 0: u_normal_baseline = u_normal else: angle = np.arccos( np.clip(np.dot(u_normal_baseline, u_normal), -1.0, 1.0) ) if angle > np.pi / 2: u_normal = -u_normal # record u_normal inlet_facet_normals[idx, :] = u_normal # Average normal and d (we will assign this later to all facets) normal_avg = np.mean(inlet_facet_normals, axis=0) inlet_coords = np.array(vectorData["boundaries/coordinates"][inlet_nodes]) point_avg = np.mean(inlet_coords, axis=0) d_avg = -point_avg.dot(normal_avg) # plane coefficient # Standard deviation of components of normal vector normal_stdev = np.std(inlet_facet_normals, axis=0) if np.max(normal_stdev) > threshold_stdev: # if surface is not flat print( "Surface with ID {} is not flat: Standard deviation of facet unit\ normals is {}, greater than threshold of {}".format( inlet_id, np.max(normal_stdev), threshold_stdev ) ) # Move the inlet nodes into the average inlet plane (do same for outlets) ArrayNames = [ "boundaries/coordinates", "mesh/coordinates", "domains/coordinates", ] print("Moving nodes into a flat plane") for ArrayName in ArrayNames: vectorArray = vectorData[ArrayName] for node_id in range(len(vectorArray)): if node_id in inlet_nodes: # from https://stackoverflow.com/questions/9605556/ # how-to-project-a-point-onto-a-plane-in-3d (bobobobo) node = vectorArray[node_id, :] scalar_distance = node.dot(normal_avg) + d_avg node_inplane = node - scalar_distance * normal_avg vectorArray[node_id, :] = node_inplane fix = True if fix: vectorData.close() # Replace the original mesh file with the modified one mesh_path.unlink() mesh_path.write_bytes(flat_in_out_mesh_path.read_bytes()) print("Changes made to the mesh file") flat_in_out_mesh_path.unlink() # overwrite Paraview files for domains and boundaries boundary_file = File(str(mesh_path.with_name(mesh_path.stem + "_boundaries.pvd"))) domain_file = File(str(mesh_path.with_name(mesh_path.stem + "_domains.pvd"))) mesh, boundaries, domains = load_mesh_and_data(mesh_path) boundary_file << boundaries domain_file << domains else: print( "Surface with ID {} is flat: Standard deviation of facet unit\ normals is {}, less than threshold of {}".format( inlet_id, np.max(normal_stdev), threshold_stdev )) vectorData.close() flat_in_out_mesh_path.unlink() print("No changes made to the mesh file")