Source code for vasp.automatedPreprocessing.automated_preprocessing

#!/usr/bin/env python

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

import sys
import datetime
from pathlib import Path
import vtk

import configargparse
import numpy as np
from morphman import get_uncapped_surface, write_polydata, get_parameters, vtk_clean_polydata, \
    vtk_triangulate_surface, write_parameters, vmtk_cap_polydata, compute_centerlines, get_centerline_tolerance, \
    get_vtk_point_locator, extract_single_line, vtk_merge_polydata, get_point_data_array, smooth_voronoi_diagram, \
    create_new_surface, compute_centers, vmtk_smooth_surface, str2bool, vmtk_compute_voronoi_diagram, \
    prepare_output_surface, vmtk_compute_geometric_features, read_polydata

from vampy.automatedPreprocessing.preprocessing_common import get_centers_for_meshing, \
    dist_sphere_diam, dist_sphere_curvature, dist_sphere_constant, get_regions_to_refine, add_flow_extension, \
    write_mesh, mesh_alternative, find_boundaries, compute_flow_rate, setup_model_network, \
    radiusArrayName, scale_surface, get_furtest_surface_point, check_if_closed_surface
from vampy.automatedPreprocessing.repair_tools import find_and_delete_nan_triangles, clean_surface, print_surface_info
from vampy.automatedPreprocessing.simulate import run_simulation
from vampy.automatedPreprocessing.visualize import visualize_model
from vampy.simulation.simulation_common import print_mesh_information

from vasp.automatedPreprocessing.preprocessing_common import generate_mesh, distance_to_spheres_solid_thickness, \
    dist_sphere_spheres, convert_xml_mesh_to_hdf5, convert_vtu_mesh_to_xdmf, edge_length_evaluator, \
    check_flatten_boundary
from vasp.simulations.simulation_common import load_mesh_and_data


[docs] def save_command_to_file(file_path): """ Save the command-line arguments to a file. Args: file_path (str): Path to the file where the command-line arguments should be saved. """ # Join the command-line arguments into a single string command = ' '.join(sys.argv) # Open the file in append mode and write the command with open(file_path, 'a') as f: f.write(command + '\n')
[docs] def run_pre_processing(input_model, verbose_print, smoothing_method, smoothing_factor, smoothing_iterations, meshing_method, refine_region, has_multiple_inlets, add_flow_extensions, visualize, config_path, coarsening_factor, inlet_flow_extension_length, outlet_flow_extension_length, number_of_sublayers_fluid, number_of_sublayers_solid, edge_length, region_points, compress_mesh, scale_factor, scale_factor_h5, resampling_step, meshing_parameters, remove_all, solid_thickness, solid_thickness_parameters, mesh_format, flow_rate_factor, solid_side_wall_id, interface_fsi_id, solid_outer_wall_id, fluid_volume_id, solid_volume_id, mesh_generation_retries, no_solid, extract_branch, branch_group_ids, branch_ids_offset, distance_method): """ Automatically generate mesh of surface model in .vtu and .xml format, including prescribed flow rates at inlet and outlet based on flow network model. Runs simulation of meshed case on a remote ssh server if server configuration is provided. Args: input_model (str): Name of case verbose_print (bool): Toggles verbose mode smoothing_method (str): Method for surface smoothing smoothing_factor (float): Smoothing factor of Voronoi smoothing smoothing_iterations (int): Number of smoothing iterations for Taubin and Laplace smoothing meshing_method (str): Determines what the density of the volumetric mesh depends upon refine_region (bool): Refines selected region of input if True has_multiple_inlets (bool): Specifies whether the input model has multiple inlets add_flow_extensions (bool): Adds flow extensions to mesh if True visualize (bool): Visualize resulting surface model with flow rates config_path (str): Path to configuration file for remote simulation coarsening_factor (float): Refine or coarsen the standard mesh size with given factor inlet_flow_extension_length (float): Factor defining length of flow extensions at the inlet(s) outlet_flow_extension_length (float): Factor defining length of flow extensions at the outlet(s) number_of_sublayers_fluid (int): Number of sublayers for fluid number_of_sublayers_solid (int): Number of sublayers for solid edge_length (float): Edge length used for meshing with constant element size region_points (list): User defined points to define which region to refine compress_mesh (bool): Compresses finalized mesh if True scale_factor (float): Scale input model by this factor resampling_step (float): Float value determining the resampling step for centerline computations, in [m] meshing_parameters (list): Parameters for meshing method 'distancetospheres' remove_all (bool): Remove mesh and all pre-processing files solid_thickness (str): Constant or variable mesh thickness solid_thickness_parameters (list): Specify parameters for solid thickness mesh_format (str): Specify the format for the generated mesh flow_rate_factor (float): Flow rate factor solid_side_wall_id (int): ID for solid side wall interface_fsi_id (int): ID for the FSI interface solid_outer_wall_id (int): ID for the solid outer wall fluid_volume_id (int): ID for the fluid volume solid_volume_id (int): ID for the solid volume mesh_generation_retries (int): Number of mesh generation retries before trying alternative method no_solid (bool): Generate mesh without solid extract_branch (bool): Enable extraction of a specific branch, marking solid mesh IDs with an offset. branch_group_ids (list): Specify group IDs to extract for the branch. branch_ids_offset (int): Set offset for marking solid mesh IDs when extracting a branch. distance_method (str): Change between 'euclidean' and 'geodesic' distance measure """ # Get paths input_model_path = Path(input_model) case_name = input_model_path.stem dir_path = input_model_path.parent print(f"\n--- Working on case: {case_name} \n") # Get unique time stamp and save given command to file time_stamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") save_command_to_file(dir_path / f"commands_{time_stamp}.txt") # Naming conventions base_path = dir_path / case_name file_name_centerlines = base_path.with_name(base_path.name + "_centerlines.vtp") file_name_refine_region_centerlines = base_path.with_name(base_path.name + "_refine_region_centerline.vtp") file_name_region_centerlines = base_path.with_name(base_path.name + "_region_centerline_{}.vtp") file_name_distance_to_sphere_diam = base_path.with_name(base_path.name + "_distance_to_sphere_diam.vtp") file_name_distance_to_sphere_const = base_path.with_name(base_path.name + "_distance_to_sphere_const.vtp") file_name_distance_to_sphere_curv = base_path.with_name(base_path.name + "_distance_to_sphere_curv.vtp") file_name_distance_to_sphere_spheres = base_path.with_name(base_path.name + "_distance_to_sphere_spheres.vtp") file_name_distance_to_sphere_solid_thickness = \ base_path.with_name(base_path.name + "_distance_to_sphere_solid_thickness.vtp") file_name_parameters = base_path.with_name(base_path.name + "_info.json") file_name_probe_points = base_path.with_name(base_path.name + "_probe_point.json") file_name_voronoi = base_path.with_name(base_path.name + "_voronoi.vtp") file_name_voronoi_smooth = base_path.with_name(base_path.name + "_voronoi_smooth.vtp") file_name_voronoi_surface = base_path.with_name(base_path.name + "_voronoi_surface.vtp") file_name_surface_smooth = base_path.with_name(base_path.name + "_smooth.vtp") file_name_model_flow_ext = base_path.with_name(base_path.name + "_flowext.vtp") file_name_clipped_model = base_path.with_name(base_path.name + "_clippedmodel.vtp") file_name_flow_centerlines = base_path.with_name(base_path.name + "_flow_cl.vtp") file_name_surface_name = base_path.with_name(base_path.name + "_remeshed_surface.vtp") file_name_xml_mesh = base_path.with_suffix(".xml") file_name_vtu_mesh = base_path.with_suffix(".vtu") file_name_hdf5_mesh = base_path.with_suffix(".h5") file_name_xdmf_mesh = base_path.with_suffix(".xdmf") file_name_edge_length_xdmf = base_path.with_name(base_path.name + "_edge_length.xdmf") region_centerlines = None if remove_all: print("--- Removing mesh and all pre-processing files\n") files_to_remove = [ file_name_centerlines, file_name_refine_region_centerlines, file_name_region_centerlines, file_name_distance_to_sphere_diam, file_name_distance_to_sphere_const, file_name_distance_to_sphere_curv, file_name_distance_to_sphere_spheres, file_name_distance_to_sphere_solid_thickness, file_name_parameters, file_name_probe_points, file_name_voronoi, file_name_voronoi_smooth, file_name_voronoi_surface, file_name_surface_smooth, file_name_model_flow_ext, file_name_clipped_model, file_name_flow_centerlines, file_name_surface_name, file_name_xml_mesh, file_name_vtu_mesh, file_name_hdf5_mesh, file_name_xdmf_mesh, ] for file_path in files_to_remove: if file_path.exists(): file_path.unlink() # Open the surface file. print("--- Load model file\n") surface = read_polydata(input_model) # Scale surface if scale_factor is not None: print(f"--- Scale model by factor {scale_factor}\n") surface = scale_surface(surface, scale_factor) resampling_step *= scale_factor # Check if surface is closed and uncapps model if True if check_if_closed_surface(surface): if not file_name_clipped_model.is_file(): print("--- Clipping the models inlets and outlets.\n") # Value of gradients_limit should be generally low, to detect flat surfaces corresponding # to closed boundaries. Area_limit will set an upper limit of the detected area, may vary between models. # The circleness_limit parameters determines the detected regions' similarity to a circle, often assumed # to be close to a circle. surface = get_uncapped_surface(surface, gradients_limit=0.01, area_limit=20, circleness_limit=5) write_polydata(surface, str(file_name_clipped_model)) else: surface = read_polydata(str(file_name_clipped_model)) # Get model parameters parameters = get_parameters(str(base_path)) if "check_surface" not in parameters.keys(): surface = vtk_clean_polydata(surface) surface = vtk_triangulate_surface(surface) # Check the mesh if there is redundant nodes or NaN triangles. print_surface_info(surface) find_and_delete_nan_triangles(surface) surface = clean_surface(surface) if find_and_delete_nan_triangles(surface): raise RuntimeError(("There is an issue with the surface. " "Nan coordinates or some other shenanigans.")) else: parameters["check_surface"] = True write_parameters(parameters, str(base_path)) # Create a capped version of the surface capped_surface = vmtk_cap_polydata(surface) # Get centerlines print("--- Get centerlines\n") inlet, outlets = get_centers_for_meshing(surface, has_multiple_inlets, str(base_path)) num_inlets_outlets = len(inlet) // 3 + len(outlets) // 3 # Get point the furthest away from the inlet when only one boundary has_outlet = len(outlets) != 0 if not has_outlet: outlets = get_furtest_surface_point(inlet, surface) source = outlets if has_multiple_inlets else inlet target = inlet if has_multiple_inlets else outlets centerlines, voronoi, _ = compute_centerlines(source, target, str(file_name_centerlines), capped_surface, resampling=resampling_step) print("\n") tol = get_centerline_tolerance(centerlines) # Get 'center' and 'radius' of the regions(s) region_center = [] misr_max = [] if refine_region: regions = get_regions_to_refine(capped_surface, region_points, str(base_path)) for i in range(len(regions) // 3): print( f"--- Region to refine ({i + 1}): " + f"{regions[3 * i]:.3f} {regions[3 * i + 1]:.3f} {regions[3 * i + 2]:.3f}\n" ) centerline_region, _, _ = compute_centerlines(source, regions, str(file_name_refine_region_centerlines), capped_surface, resampling=resampling_step) # Extract the region centerline refine_region_centerline = [] info = get_parameters(str(base_path)) number_of_regions = info["number_of_regions"] # Compute mean distance between points for i in range(number_of_regions): file_name_region_centerlines_i = Path(str(file_name_region_centerlines).format(i)) if not file_name_region_centerlines_i.is_file(): line = extract_single_line(centerline_region, i) locator = get_vtk_point_locator(centerlines) for j in range(line.GetNumberOfPoints() - 1, 0, -1): point = line.GetPoints().GetPoint(j) ID = locator.FindClosestPoint(point) tmp_point = centerlines.GetPoints().GetPoint(ID) dist = np.sqrt(np.sum((np.asarray(point) - np.asarray(tmp_point)) ** 2)) if dist <= tol: break tmp = extract_single_line(line, 0, start_id=j) write_polydata(tmp, str(file_name_region_centerlines_i)) # List of VtkPolyData sac(s) centerline refine_region_centerline.append(tmp) else: refine_region_centerline.append(read_polydata(str(file_name_region_centerlines_i))) # Merge the refined region centerline region_centerlines = vtk_merge_polydata(refine_region_centerline) for region in refine_region_centerline: region_factor = 0.9 if has_multiple_inlets else 0.5 region_center.append(region.GetPoints().GetPoint(int(region.GetNumberOfPoints() * region_factor))) tmp_misr = get_point_data_array(radiusArrayName, region) misr_max.append(tmp_misr.max()) # Smooth surface if smoothing_method == "voronoi": print("--- Smooth surface: Voronoi smoothing\n") if not file_name_surface_smooth.is_file(): # Get Voronoi diagram if not file_name_voronoi.is_file(): voronoi = vmtk_compute_voronoi_diagram(capped_surface, str(file_name_voronoi)) write_polydata(voronoi, str(file_name_voronoi)) else: voronoi = read_polydata(str(file_name_voronoi)) # Get smooth Voronoi diagram if not file_name_voronoi_smooth.is_file(): voronoi_smoothed = smooth_voronoi_diagram(voronoi, centerlines, smoothing_factor, no_smooth_cl=region_centerlines) write_polydata(voronoi_smoothed, str(file_name_voronoi_smooth)) else: voronoi_smoothed = read_polydata(str(file_name_voronoi_smooth)) # Create new surface from the smoothed Voronoi surface_smoothed = create_new_surface(voronoi_smoothed) # Uncapp the surface surface_uncapped = prepare_output_surface(surface_smoothed, surface, centerlines, str(file_name_voronoi_surface), test_merge=True) # Check if there has been added new outlets num_outlets = centerlines.GetNumberOfLines() inlets, outlets = compute_centers(surface_uncapped) num_outlets_after = len(outlets) // 3 if num_outlets != num_outlets_after: write_polydata(surface, str(file_name_surface_smooth)) print(f"ERROR: Automatic clipping failed. You have to open {file_name_surface_smooth} and " + "manually clip the branch which is still capped. " + f"Overwrite the current {file_name_surface_smooth} and restart the script.") sys.exit(-1) surface = surface_uncapped # Smoothing to improve the quality of the elements surface = vmtk_smooth_surface(surface, "laplace", iterations=200) # Write surface write_polydata(surface, str(file_name_surface_smooth)) else: surface = read_polydata(str(file_name_surface_smooth)) elif smoothing_method in ["laplace", "taubin"]: print(f"--- Smooth surface: {smoothing_method.capitalize()} smoothing\n") if not file_name_surface_smooth.is_file(): surface = vmtk_smooth_surface(surface, smoothing_method, iterations=smoothing_iterations, passband=0.1, relaxation=0.01) # Save the smoothed surface write_polydata(surface, str(file_name_surface_smooth)) else: surface = read_polydata(str(file_name_surface_smooth)) elif smoothing_method == "no_smooth" or None: print("--- No smoothing of surface\n") else: raise ValueError(f"Unknown smoothing method: {smoothing_method}") # Add flow extensions if add_flow_extensions: if not file_name_model_flow_ext.is_file(): print("--- Adding flow extensions\n") # Add extension normal on boundary for models with multiple inlets extension = "centerlinedirection" if has_multiple_inlets else "boundarynormal" if has_multiple_inlets: # Flip lengths if model has multiple inlets inlet_flow_extension_length, outlet_flow_extension_length = \ outlet_flow_extension_length, inlet_flow_extension_length # Add extensions to inlet (artery) surface_extended = add_flow_extension(surface, centerlines, is_inlet=True, extension_length=inlet_flow_extension_length, extension_mode=extension) # Add extensions to outlets (artery) surface_extended = add_flow_extension(surface_extended, centerlines, is_inlet=False, extension_length=outlet_flow_extension_length) surface_extended = vmtk_smooth_surface(surface_extended, "laplace", iterations=200) write_polydata(surface_extended, str(file_name_model_flow_ext)) else: surface_extended = read_polydata(str(file_name_model_flow_ext)) else: print("--- Not adding flow extensions\n") surface_extended = surface # Capp surface with flow extensions capped_surface = vmtk_cap_polydata(surface_extended) # Get new centerlines with the flow extensions if add_flow_extensions: if not file_name_flow_centerlines.is_file(): print("--- Compute the model centerlines with flow extension.\n") # Compute the centerlines. if has_outlet: inlet, outlets = get_centers_for_meshing(surface_extended, has_multiple_inlets, str(base_path), use_flow_extensions=True) else: inlet, _ = get_centers_for_meshing(surface_extended, has_multiple_inlets, str(base_path), use_flow_extensions=True) # Flip outlets and inlets for models with multiple inlets source = outlets if has_multiple_inlets else inlet target = inlet if has_multiple_inlets else outlets centerlines, _, _ = compute_centerlines(source, target, str(file_name_flow_centerlines), capped_surface, resampling=resampling_step) else: centerlines = read_polydata(str(file_name_flow_centerlines)) # Clip centerline if only one inlet to avoid refining model surface if not has_outlet: line = extract_single_line(centerlines, 0) line = vmtk_compute_geometric_features(line, smooth=False) # Clip centerline where Frenet Tangent is constant n = get_point_data_array("FrenetTangent", line, k=3) n_diff = np.linalg.norm(np.cross(n[1:], n[:-1]), axis=1) n_id = n_diff[::-1].argmax() centerlines = extract_single_line(centerlines, 0, end_id=centerlines.GetNumberOfPoints() - n_id - 1) # Choose input for the mesh print("--- Computing distance to sphere\n") if meshing_method == "constant": if not file_name_distance_to_sphere_const.is_file(): distance_to_sphere = dist_sphere_constant(surface_extended, centerlines, region_center, misr_max, str(file_name_distance_to_sphere_const), edge_length) else: distance_to_sphere = read_polydata(str(file_name_distance_to_sphere_const)) elif meshing_method == "curvature": if not file_name_distance_to_sphere_curv.is_file(): distance_to_sphere = dist_sphere_curvature(surface_extended, centerlines, region_center, misr_max, str(file_name_distance_to_sphere_curv), coarsening_factor) else: distance_to_sphere = read_polydata(str(file_name_distance_to_sphere_curv)) elif meshing_method == "diameter": if not file_name_distance_to_sphere_diam.is_file(): distance_to_sphere = dist_sphere_diam(surface_extended, centerlines, region_center, misr_max, str(file_name_distance_to_sphere_diam), coarsening_factor) else: distance_to_sphere = read_polydata(str(file_name_distance_to_sphere_diam)) elif meshing_method == "distancetospheres": if not file_name_distance_to_sphere_spheres.is_file(): distance_to_sphere = surface_extended if len(meshing_parameters) % 4 == 0: times_to_run = len(meshing_parameters) // 4 for i in range(times_to_run): meshing_params = meshing_parameters[i * 4:(i + 1) * 4] if scale_factor is not None: meshing_params[0] *= scale_factor meshing_params[2] *= scale_factor meshing_params[3] *= scale_factor distance_to_sphere = dist_sphere_spheres(distance_to_sphere, file_name_distance_to_sphere_spheres, *meshing_params, distance_method=distance_method) else: print("ERROR: Invalid parameters for meshing method 'distancetospheres'. This should be " + "given as multiple of four parameters: 'offset', 'scale', 'min' and 'max.") sys.exit(-1) else: distance_to_sphere = read_polydata(str(file_name_distance_to_sphere_spheres)) # Get solid thickness if solid_thickness == 'variable': if not file_name_distance_to_sphere_solid_thickness.is_file(): if len(solid_thickness_parameters) == 4: # Apply scale factor to offset, min distance, and max distance if scale_factor is not None: solid_thickness_parameters[0] *= scale_factor # Offset solid_thickness_parameters[2] *= scale_factor # Min distance solid_thickness_parameters[3] *= scale_factor # Max distance distance_to_sphere = distance_to_spheres_solid_thickness(distance_to_sphere, file_name_distance_to_sphere_solid_thickness, *solid_thickness_parameters) elif len(solid_thickness_parameters) == 0: distance_to_sphere = distance_to_spheres_solid_thickness(distance_to_sphere, file_name_distance_to_sphere_solid_thickness) else: print("ERROR: Invalid parameters for variable solid thickness. This should be " + "given as four parameters: 'offset', 'scale', 'min' and 'max.") sys.exit(-1) else: distance_to_sphere = read_polydata(str(file_name_distance_to_sphere_solid_thickness)) elif solid_thickness == "painted": file_name_aneudraw_data = base_path.with_name(base_path.name + "_aneudraw_surface.vtp") assert file_name_aneudraw_data.is_file(), "ERROR: Aneudraw file is missing." aneudraw = read_polydata(str(file_name_aneudraw_data)) thickness_array = aneudraw.GetPointData().GetArray("Thickness") # Create points for interpolation points = vtk.vtkPolyData() points.SetPoints(aneudraw.GetPoints()) points.GetPointData().AddArray(thickness_array) # Use a Gaussian kernel for interpolation gaussian_kernel = vtk.vtkGaussianKernel() gaussian_kernel.SetRadius(solid_thickness_parameters[1]) gaussian_kernel.SetSharpness(solid_thickness_parameters[2]) # Set up the interpolator with a Gaussian kernel interpolator = vtk.vtkPointInterpolator() interpolator.SetInputData(surface_extended) interpolator.SetSourceData(points) interpolator.SetNullValue(solid_thickness_parameters[0]) interpolator.SetKernel(gaussian_kernel) interpolator.Update() interpolated = interpolator.GetOutput() aneudraw_interpolated_file = base_path.with_name(base_path.name + "_aneudraw_interpolated.vtp") write_polydata(interpolated, str(aneudraw_interpolated_file)) thickness_array_interpolated = interpolated.GetPointData().GetArray("Thickness") distance_to_sphere.GetPointData().AddArray(thickness_array_interpolated) print("--- Constructed solid thickness from painted surface mesh\n") else: if len(solid_thickness_parameters) != 1 or solid_thickness_parameters[0] <= 0: print("ERROR: Invalid parameter for constant solid thickness. This should be a " + "single number greater than zero.") sys.exit(-1) else: # Apply scale factor to the constant thickness value if scale_factor is not None: solid_thickness_parameters[0] *= scale_factor # Compute mesh if not file_name_vtu_mesh.is_file(): print("--- Generating FSI mesh\n") def try_generate_mesh(distance_to_sphere, number_of_sublayers_fluid, number_of_sublayers_solid, solid_thickness, solid_thickness_parameters, centerlines): try: return generate_mesh(distance_to_sphere, number_of_sublayers_fluid, number_of_sublayers_solid, solid_thickness, solid_thickness_parameters, centerlines, solid_side_wall_id, interface_fsi_id, solid_outer_wall_id, fluid_volume_id, solid_volume_id, no_solid, extract_branch, branch_group_ids, branch_ids_offset) except RuntimeError: return None mesh_generation_failed = True for i in range(mesh_generation_retries + 1): mesh_and_surface = try_generate_mesh(distance_to_sphere, number_of_sublayers_fluid, number_of_sublayers_solid, solid_thickness, solid_thickness_parameters, centerlines) if mesh_and_surface: mesh, remeshed_surface = mesh_and_surface mesh_generation_failed = False break if mesh_generation_failed: print(f"ERROR: Mesh generation failed after {mesh_generation_retries} retries. " "Trying to remesh with an alternative method.") distance_to_sphere = mesh_alternative(distance_to_sphere) mesh_and_surface = try_generate_mesh(distance_to_sphere, number_of_sublayers_fluid, number_of_sublayers_solid, solid_thickness, solid_thickness_parameters, centerlines) if mesh_and_surface: mesh, remeshed_surface = mesh_and_surface else: print("ERROR: Mesh generation failed with an alternative method.") sys.exit(-1) assert mesh.GetNumberOfPoints() > 0, "No points in mesh, try to remesh." assert remeshed_surface.GetNumberOfPoints() > 0, "No points in surface mesh, try to remesh." if mesh_format in ("xml", "hdf5"): write_mesh(compress_mesh, str(file_name_surface_name), str(file_name_vtu_mesh), str(file_name_xml_mesh), mesh, remeshed_surface) else: # Write mesh in VTU format write_polydata(remeshed_surface, str(file_name_surface_name)) write_polydata(mesh, str(file_name_vtu_mesh)) # Write the mesh ID's to parameter file parameters["solid_side_wall_id"] = solid_side_wall_id parameters["interface_fsi_id"] = interface_fsi_id parameters["solid_outer_wall_id"] = solid_outer_wall_id parameters["fluid_volume_id"] = fluid_volume_id parameters["solid_volume_id"] = solid_volume_id parameters["branch_ids_offset"] = branch_ids_offset write_parameters(parameters, str(base_path)) else: mesh = read_polydata(str(file_name_vtu_mesh)) # Add .gz to XML mesh file if compressed if compress_mesh: file_name_xml_mesh = Path(f"{file_name_xml_mesh}.gz") if mesh_format == "hdf5": print("--- Converting XML mesh to HDF5\n") convert_xml_mesh_to_hdf5(file_name_xml_mesh, scale_factor_h5) # NOTE: stdev threshold is currently hardcoded, not sure if this should be a command line argument print("--- Flattening the inlet/outlet if needed\n") check_flatten_boundary(num_inlets_outlets, file_name_hdf5_mesh, threshold_stdev=0.001) # Evaluate edge length for inspection print("--- Evaluating edge length\n") edge_length_evaluator(file_name_xml_mesh, file_name_edge_length_xdmf) # Print mesh information dolfin_mesh, _, _ = load_mesh_and_data(file_name_hdf5_mesh) print_mesh_information(dolfin_mesh) elif mesh_format == "xdmf": print("--- Converting VTU mesh to XDMF\n") convert_vtu_mesh_to_xdmf(file_name_vtu_mesh, file_name_xdmf_mesh) # Evaluate edge length for inspection print("--- Evaluating edge length\n") edge_length_evaluator(file_name_xdmf_mesh, file_name_edge_length_xdmf) network, probe_points = setup_model_network(centerlines, str(file_name_probe_points), region_center, verbose_print, has_multiple_inlets) # Load updated parameters following meshing parameters = get_parameters(str(base_path)) print("--- Computing flow rates and flow split, and setting boundary IDs\n") mean_inflow_rate = compute_flow_rate(has_multiple_inlets, inlet, parameters, flow_rate_factor) find_boundaries(str(base_path), mean_inflow_rate, network, mesh, verbose_print, has_multiple_inlets) # Load updated parameters after setting boundary ID's parameters = get_parameters(str(base_path)) # Determine the key for inlet and outlet based on whether we have multiple inlets or not inlet_key = "inlet_ids" if has_multiple_inlets else "inlet_id" outlet_key = "outlet_id" if has_multiple_inlets else "outlet_ids" # Adjust inlet and outlet ID's by incrementing each by 1 for key in [inlet_key, outlet_key]: parameters[key] = [val + 1 for val in parameters[key]] # Write parameters back to file after adjusting inlet and outlet ID's write_parameters(parameters, str(base_path)) # Display the flow split at the outlets, inlet flow rate, and probes. if visualize: print("--- Visualizing flow split at outlets, inlet flow rate, and probes in VTK render window. ") print("--- Press 'q' inside the render window to exit.") visualize_model(network.elements, probe_points, surface_extended, mean_inflow_rate) # Start simulation though ssh, without password if config_path is not None: print("--- Uploading mesh and simulation files to cluster. Queueing simulation and post-processing.") run_simulation(config_path, dir_path, case_name) print("--- Removing unused pre-processing files") files_to_remove = [ file_name_centerlines, file_name_refine_region_centerlines, file_name_region_centerlines, file_name_distance_to_sphere_diam, file_name_distance_to_sphere_const, file_name_distance_to_sphere_curv, file_name_voronoi, file_name_voronoi_smooth, file_name_voronoi_surface, file_name_surface_smooth, file_name_model_flow_ext, file_name_clipped_model, file_name_flow_centerlines, file_name_surface_name ] # In case of painted mesh, other files are necessary to create identical mesh with constant thickness if not solid_thickness == "painted": for file_path in files_to_remove: if file_path.exists(): file_path.unlink()
[docs] def read_command_line(input_path=None): """ Read arguments from commandline and return all values in a dictionary. If input_path is not None, then do not parse command line, but only return default values. Args: input_path (str): Input file path, positional argument with default None. """ parser = configargparse.ArgumentParser(formatter_class=configargparse.ArgumentDefaultsHelpFormatter, description="Automated pre-processing for vascular modeling.") # Add common arguments required = input_path is None v = parser.add_mutually_exclusive_group(required=False) v.add_argument('-v', '--verbosity', dest='verbosity', action='store_true', default=False, help="Activates the verbose mode.") parser.add_argument('--config', is_config_file=True, help='Configuration file') parser.add_argument('-i', '--input-model', type=str, required=True, help="Path to input model containing the 3D model. Expected format is VTK/VTP or STL.") parser.add_argument('-cm', '--compress-mesh', type=str2bool, required=False, default=True, help="Compress output mesh after generation.") parser.add_argument('-sm', '--smoothing-method', type=str, required=False, default="no_smooth", choices=["voronoi", "no_smooth", "laplace", "taubin"], help="Determines smoothing method for surface smoothing. For Voronoi smoothing you can " + "control the smoothing factor with --smoothing-factor (default = 0.25). For Laplace " + "and Taubin smoothing, you can controll the amount of smoothing iterations with " + "--smothing-iterations (default = 800).") parser.add_argument('-c', '--coarsening-factor', type=float, required=False, default=1.0, help="Refine or coarsen the standard mesh size. The higher the value the coarser the mesh.") parser.add_argument('-sf', '--smoothing-factor', type=float, required=False, default=0.25, help="Smoothing factor for Voronoi smoothing, removes all spheres which" + " has a radius < MISR*(1-0.25), where MISR varying along the centerline.") parser.add_argument('-si', '--smoothing-iterations', type=int, required=False, default=800, help="Number of smoothing iterations for Laplace and Taubin type smoothing.") parser.add_argument('-m', '--meshing-method', type=str, choices=["diameter", "curvature", "constant", "distancetospheres"], default="diameter", help="Determines method of meshing. The method 'constant' is supplied with a constant edge " + "length controlled by the -el argument, resulting in a constant density mesh. " + "The 'curvature' method and 'diameter' method produces a variable density mesh, " + "based on the surface curvature and the distance from the " + "centerline to the surface, respectively. The 'distancetospheres' method produces a " + "variable density mesh based on a distance array given by user specified points " + "(spheres). Using this method will open an interactive window which allows to place " + "spheres where the cursor is pointing by pressing 'space'. By pressing 'd', the " + "surface is colored by the distance to the spheres. By pressing 'a', a scaling " + "function can be specified by four parameters: 'offset', 'scale', 'min' and 'max'. " + "These parameters for the scaling function can also be controlled by the -mp argument.") parser.add_argument('-el', '--edge-length', default=None, type=float, help="Characteristic edge length used for the 'constant' meshing method.") refine_region = parser.add_mutually_exclusive_group(required=False) refine_region.add_argument('-r', '--refine-region', action='store_true', default=False, help="Determine whether or not to refine a specific region of " + "the input model") parser.add_argument('-rp', '--region-points', type=float, nargs="+", default=None, help="If -r or --refine-region is True, the user can provide the point(s)" " which defines the regions to refine. " + "Example providing the points (0.1, 5.0, -1) and (1, -5.2, 3.21):" + " --region-points 0.1 5 -1 1 5.24 3.21") multiple_inlets = parser.add_mutually_exclusive_group(required=False) multiple_inlets.add_argument('-hmi', '--has-multiple-inlets', action="store_true", default=False, help="Specifies whether the input model has multiple inlets. When set to True, it " + "indicates a configuration with multiple inlets and one outlet.") parser.add_argument('-f', '--add-flowextensions', default=True, type=str2bool, help="Add flow extensions to the model.") parser.add_argument('-fli', '--inlet-flowextension', default=5, type=float, help="Length of flow extensions at inlet(s).") parser.add_argument('-flo', '--outlet-flowextension', default=5, type=float, help="Length of flow extensions at outlet(s).") parser.add_argument('-nbf', '--number-of-sublayers-fluid', default=2, type=int, help="Number of sublayers in the fluid domain.") parser.add_argument('-nbs', '--number-of-sublayers-solid', default=2, type=int, help="Number of sublayers in the solid domain.") parser.add_argument('-viz', '--visualize', default=True, type=str2bool, help="Visualize surface, inlet, outlet and probes after meshing.") parser.add_argument('-cp', '--config-path', type=str, default=None, help='Path to configuration file for remote simulation. ' + 'See ssh_config.json for details') parser.add_argument('-sc', '--scale-factor', default=None, type=float, help="Scale input model by this factor. Used to scale model to [mm].") parser.add_argument("-sch5", "--scale-factor-h5", default=1.0, type=float, help="Scaling factor for HDF5 mesh. Used to scale model to [mm]. " + "Note that probes and other parameters are not scaled. " + "Do not use in combination with --scale-factor.") parser.add_argument('-rs', '--resampling-step', default=0.1, type=float, help="Resampling step used to resample centerline in [m]. " + "Note: If --scale-factor is used, this step will be adjusted accordingly.") parser.add_argument('-mp', '--meshing-parameters', type=float, nargs="+", default=[0, 0.1, 0.4, 0.6], help="Parameters for meshing method 'distancetospheres'. This should be given as " + "multiple sets of four numbers for the distancetosphere scaling function: 'offset', " + "'scale', 'min', and 'max'. Each set of four numbers corresponds to a separate run of " + "the function. For example --meshing-parameters 0 0.1 0.5 0.8 0 0.2 0.3 0.8 will run " + "the function twice. " + "Note: If --scale-factor is used, 'offset', 'min', and 'max' parameters will be " + "adjusted accordingly for each run.") parser.add_argument('-dm', '--distance-method', type=str, choices=["euclidean", "geodesic"], default="geodesic", help="Determines method of computing distance between point of refinement and surface.") remove_all = parser.add_mutually_exclusive_group(required=False) remove_all.add_argument('-ra', '--remove-all', action="store_true", default=False, help="Remove mesh and all pre-processing files.") parser.add_argument('-st', '--solid-thickness', type=str, choices=["constant", "variable", "painted"], default="constant", help="Determines whether to use constant, variable, or painted thickness for the solid. " + "Use --solid-thickness-parameters to adjust distancetospheres parameters " + "when using variable thickness. To use pained thickness, the mesh with _aneudraw " + "suffix, which contains thickness information, must be present in the same " + "directory as the input model.") parser.add_argument('-stp', '--solid-thickness-parameters', type=float, nargs="+", default=[0.3], help="Parameters for solid thickness [m]. For 'constant' solid thickness, provide a single " + "float. For 'variable' solid thickness, provide four floats for the distancetosphere " + "scaling function: 'offset', 'scale', 'min', and 'max'. " + "For example, --solid-thickness-parameters 0 0.1 0.25 0.3. " + "For 'painted' solid thickness, provide three floats: the first value is the null value " + "used when no nearby thickness data is available, the second value is the radius of " + "the Gaussian kernel for interpolation, and the third value is the sharpness parameter" + "of the Gaussian kernel to adjust the falloff rate." + "For example, --solid-thickness-parameters 0.3 0.4 1.0." + "Note: If --scale-factor is used, 'offset', 'min', and 'max' parameters will be " + "adjusted accordingly for 'variable' solid thickness, and the constant value will " + "also be scaled for 'constant' solid thickness." + "Defaults: [0.3] for 'constant', [0.3, 0.4, 1.0] for 'painted'.") parser.add_argument('-mf', '--mesh-format', type=str, choices=["xml", "hdf5", "xdmf"], default="hdf5", help="Specify the format for the generated mesh. Available options: 'xml', 'hdf5', 'xdmf'.") parser.add_argument('-fr', '--flow-rate-factor', default=0.31, type=float, help="Flow rate factor.") parser.add_argument("--solid-side-wall-id", type=int, default=11, help="ID for solid side wall") parser.add_argument("--interface-fsi-id", type=int, default=22, help="ID for the FSI interface") parser.add_argument("--solid-outer-wall-id", type=int, default=33, help="ID for the solid outer wall") parser.add_argument("--fluid-volume-id", type=int, default=0, help="ID for the fluid volume") parser.add_argument("--solid-volume-id", type=int, default=1, help="ID for the solid volume") parser.add_argument("-mgr", "--mesh-generation-retries", type=int, default=4, help="Number of mesh generation retries before trying to subdivide and smooth the " + "input model") no_solid = parser.add_mutually_exclusive_group(required=False) no_solid.add_argument('-ns', '--no-solid', action="store_true", default=False, help="Generate mesh without solid.") extract_branch = parser.add_mutually_exclusive_group(required=False) extract_branch.add_argument('-eb', '--extract-branch', action="store_true", default=False, help="Enable extraction of a specific branch, marking solid mesh IDs with an offset. " "The offset can be controlled by the --branch-ids-offset option. This option is " "relevant for marking specific branches, such as arteries and veins, in an AVF.") parser.add_argument('-bg', '--branch-group-ids', type=int, nargs="+", default=[], help="Specify the group IDs to extract for the branch. If not used, an interactive window will " "prompt the user to select the group IDs for marking.") parser.add_argument('-bo', '--branch-ids-offset', default=1000, type=int, help="Set the offset for marking solid mesh IDs when extracting a branch.") # Parse path to get default values if required: args = parser.parse_args() else: args = parser.parse_args(["-i" + input_path]) if args.meshing_method == "constant" and args.edge_length is None: raise ValueError("ERROR: Please provide the edge length for uniform density meshing using --edge-length.") if args.refine_region and args.region_points is not None: if len(args.region_points) % 3 != 0: raise ValueError("ERROR: Please provide the region points as a multiple of 3.") # Check if --solid-thickness is 'painted' and set default values accordingly if args.solid_thickness == "painted" and len(args.solid_thickness_parameters) == 1: args.solid_thickness_parameters = [args.solid_thickness_parameters[0], 0.4, 1.0] elif args.solid_thickness == "painted" and len(args.solid_thickness_parameters) != 3: raise ValueError("ERROR: solid thickness parameters for 'painted' thickness should be three floats.") if args.verbosity: print() print("--- VERBOSE MODE ACTIVATED ---") def verbose_print(*args): for arg in args: print(arg, end=' ') print() else: def verbose_print(*args): return None verbose_print(args) return dict(input_model=args.input_model, verbose_print=verbose_print, smoothing_method=args.smoothing_method, smoothing_factor=args.smoothing_factor, smoothing_iterations=args.smoothing_iterations, meshing_method=args.meshing_method, refine_region=args.refine_region, has_multiple_inlets=args.has_multiple_inlets, add_flow_extensions=args.add_flowextensions, config_path=args.config_path, edge_length=args.edge_length, coarsening_factor=args.coarsening_factor, inlet_flow_extension_length=args.inlet_flowextension, number_of_sublayers_fluid=args.number_of_sublayers_fluid, number_of_sublayers_solid=args.number_of_sublayers_solid, visualize=args.visualize, region_points=args.region_points, compress_mesh=args.compress_mesh, outlet_flow_extension_length=args.outlet_flowextension, scale_factor=args.scale_factor, scale_factor_h5=args.scale_factor_h5, resampling_step=args.resampling_step, meshing_parameters=args.meshing_parameters, remove_all=args.remove_all, solid_thickness=args.solid_thickness, solid_thickness_parameters=args.solid_thickness_parameters, mesh_format=args.mesh_format, flow_rate_factor=args.flow_rate_factor, solid_side_wall_id=args.solid_side_wall_id, interface_fsi_id=args.interface_fsi_id, solid_outer_wall_id=args.solid_outer_wall_id, fluid_volume_id=args.fluid_volume_id, solid_volume_id=args.solid_volume_id, mesh_generation_retries=args.mesh_generation_retries, no_solid=args.no_solid, extract_branch=args.extract_branch, branch_group_ids=args.branch_group_ids, branch_ids_offset=args.branch_ids_offset, distance_method=args.distance_method)
[docs] def main_meshing(): run_pre_processing(**read_command_line())
if __name__ == "__main__": run_pre_processing(**read_command_line())