3.2. Meshing with snappyHexMesh#

Introduction#

As an alternative to the in-house surface-meshing tool, our platform also offers surface-mesh generation via a wrapping technique using OpenFOAM’s snappyHexMesh utility. Although snappyHexMesh was originally designed for volume meshing, the integration automatically runs the mesher and then extracts the resulting surface mesh for you.

snappyHexMesh#

snappyHexMesh is an advanced mesh generation utility included in the OpenFOAM suite, designed for creating meshes for computational fluid dynamics (CFD) simulations. It is widely used due to its capability to handle complex, non-watertight geometries and support automated refinement.

The primary function of snappyHexMesh is to convert simple background meshes into complex, geometry-conforming computational meshes suitable for numerical simulations. It uses a three-step approach that enables the user to generate meshes tailored to their specific geometries and simulation requirements. snappyHexMesh is particularly useful for:

  • Meshing around intricate CAD geometries represented in STL format

  • Extracting geometry features

  • Refining mesh near feature edges, on surfaces and in defined volumetric regions

snappyHexMesh follows a systematic meshing process comprising three major stages:

  1. Base Mesh Generation

    Before snappyHexMesh can operate, a base mesh must be created using blockMesh (also called background mesh). This mesh serves as the computational domain into which snappyHexMesh will carve the geometry. The base mesh is typically a simple hexahedral block structure.

    ../../_images/background.png

    Fig. 3.2.1 The background mesh generated by blockMesh.#

  2. Mesh Refinement

    snappyHexMesh refines the mesh in designated regions so the user is able to refine different patches, regions around edges or mesh in a specified volume.

    ../../_images/refinement.png

    Fig. 3.2.2 The refined background mesh based on the mesher settings..#

  3. Surface Snapping

    The user provides the geometry in a triangulated surface format, specifically an STL ASCII file. snappyHexMesh then performs the following:

    • Feature Detection: Identifies edges and other geometric features to preserve sharp angles.

    • Surface Casting: Projects the background mesh onto the triangulated geometry.

    • Snapping: Adjusts the mesh to conform to the geometry by moving mesh points to surface intersections, ensuring high fidelity.

    ../../_images/snapping.png

    Fig. 3.2.3 The mesh snapped to the geometry.#

snappyHexMesh API in flow360#

The snappyHexMesh API processes the data based on the basically creates a snappyHexMesh case files based on a simplifed user input definitions. The snappyHexMesh integration is used through the class ModularMeshingWorkflow where the surface meshing is defined with the SnappySurfaceMeshingParams. This class needs specifications for mesh defaults (settings used on all surfaces unless specified otherwise) and allows for customizing settings for snapping, castellating, smoothing, quality metrics and refinements.

Detailed Workflow#

The general workflow which the API runs is:

  1. Geometry parsing: for a geometry with multiple solids, the code splits the geometry into bodies and puts them into different files; regions are also extracted when needed for edge extraction.

  2. Run snappyHexMesh

  3. foamToSurface: extract the surface mesh from the volume mesh surfaces.

  4. Smooth: apply lambdaMuSmoothing on the resultant mesh.

  5. Mesh processing: improve the resulting mesh so it meets the standards of the volume mesher.

Geometry preparation#

If the snappyHexMesh is used for surface meshing the geometry provided must be in the ASCII STL format. The input should NOT contain information about face or solid colors, only solid names and basic geometry information, multiple solids are allowed.

The geometry is grouped in two levels:

  • Body: a part of the geometry that will be input into snappyHexMesh as a searchableSurfaceWithGaps.

  • Region: a part of a solid that will be input as its parent’s region.

How the geometry is grouped depends on the names given to each solid instance in an stl file. The basic naming convention is “body::region”.

  • A Body will be merge of solid instances with their names beggining the same way. (meaning having the same name before the “::” split sign). For example “chassis::grill” and “chassis::mirror” will be considered a single body called “chassis”, but “chassis_other::pillar” will be a part of an another body. Bodies are referenced in flow360 using a fl.SnappyBody(body_name=”name”) class.

  • A Region is a part of a Body that represents a singular solid instance in a STL file. They are referenced by name as a Surface entity in flow360. For example to refernece a “bonnet” Region of the “chassis” Body the user has to specify ‘geo[“chassis::bonnet”]’ where geo is a geometry object that can be extracted from a Case.

Important notes:

  • The patch names have to be unique. There cannot be two solid instnaces named the same in the STL file.

  • Unnamed solid instances are not supported.

  • If the geometry contains internal patches that will be completely deleted during the meshing process, the case ran directly on a volume mesh generrated will fail. As a workaraound the user can download the volume mesh and reupload it to a new project or delete all parts of the input geometry that will be later deleted by the mesher.

Case Setup#

The python code looks excatly like the configuration using the flow360 meshing with the exception that the class ModularMeshingWorkflow is used when the meshing parameters are defined in SimulationParams. The case is specified with the following parameters in SnappySurfaceMeshingParams:

  1. Default: default minimum spacing, maximum spacing and gap resolution.

  2. Quality Metrics: values which are written to the meshQualityDict and inputs are directly parsed to the file.

  3. Snap controls: parameters which define the snapping controls as defined in snappyHexMeshDict.

  4. Castellated mesh controls: parameters which define the snapping controls as defined in snappyHexMeshDict.

  5. Smooth controls: parameters which define the smoothing process, this includes the extraction of edges with a new surfaceFeaturesDict and the inputs for the command surfaceLambdaMuSmooth from OpenFOAM.

  6. Bounding box: manually set the bounding box if not computed automatically.

  7. Zones: list of mesh zones which will be kept (defined by the points inside of those zones).

  8. Refinements: the refinements are defined as a list of surface refinement types. It is possible to do a body refinement, a region refinement, a surface edge refinement or a uniform refinement with the classes SnappyBodyRefinement, SnappyRegionRefinement, SnappySurfaceEdgeRefinement and UniformRefinement. For these refinements, spacings are given which directly translate to the levels in snappyHexMeshDict. To compute the level, the background mesh spacing is divided by the desired spacing and a logarithm with base 2 is applied to get the number of levels. This number is then rounded to the upper integer. For the edge refinements, additional information for feature extraction is needed.

Important notes:

  • When specifying a UniformRefinement, the snappyHexMesh will sometimes refine the whole underlying STL facets even they are not completely enclosed by the refinement region.

../../_images/overrefinement_base.png

Fig. 3.2.4 The example base STL geometry.#

../../_images/overrefinement.png

Fig. 3.2.5 The mesh generated from snappyHexMesh with a UniformRefinement specified.#

Case Setup Example#

The setup is build from the classes specified in the previous section. Therefore, check the API reference for further details of each method description. Here is an example code snippet which meshes the DrivAer geometry (downowdable from here):

  1import flow360 as fl
  2
  3filepath = "./your/file/path/DrivAer.stl"
  4
  5
  6project = fl.Project.from_geometry(filepath, 
  7                                   name="drivaer_snappy_example",
  8                                   solver_version="snappyHex-25.8.4",
  9                                   length_unit="mm")
 10
 11
 12# use if the geometry was already uploaded
 13#project= fl.Project.from_cloud(project_id="prj-80548c93-eade-4a1b-942b-264c08ce098d")
 14
 15geo = project.geometry
 16
 17geo.group_faces_by_tag("faceId")
 18
 19# used when the farfield is provided with the geometry, otherwise use fl.AutomatedFarfield()
 20far_field_zone = fl.UserDefinedFarfield()
 21
 22with fl.SI_unit_system:
 23    params = fl.SimulationParams(
 24        meshing=fl.ModularMeshingWorkflow(
 25            surface_meshing=fl.SnappySurfaceMeshingParams(
 26                # defaults are used for all surfaces unless specified otherwise
 27                defaults=fl.SnappySurfaceMeshingDefaults(
 28                    min_spacing=5*fl.u.mm,
 29                    max_spacing=50*fl.u.mm,
 30                    gap_resolution=0.001*fl.u.mm
 31                ),
 32                refinements=[
 33                    fl.SnappyBodyRefinement(
 34                        min_spacing=20*fl.u.mm,
 35                        max_spacing=200*fl.u.mm,
 36                        bodies=[fl.SnappyBody(body_name="chassis")]
 37                    ),
 38                    fl.SnappyBodyRefinement(
 39                        min_spacing=5*fl.u.mm,
 40                        max_spacing=200*fl.u.mm,
 41                        bodies=[fl.SnappyBody(body_name="underbody")]
 42                    ),
 43                    fl.SnappyBodyRefinement(
 44                        min_spacing=200*fl.u.mm,
 45                        max_spacing=2*fl.u.m,
 46                        bodies=[fl.SnappyBody(body_name="tunnel")]
 47                    ),
 48                    # on-edge refinement
 49                    fl.SnappySurfaceEdgeRefinement(
 50                        spacing=100*fl.u.mm,
 51                        included_angle=95*fl.u.deg,
 52                        min_elem=5,
 53                        regions=[geo["tunnel::ground*"]],
 54                        bodies=[fl.SnappyBody(body_name="tunnel")]
 55                    ),
 56                    fl.SnappyRegionRefinement(
 57                        min_spacing=3*fl.u.mm,
 58                        max_spacing=50*fl.u.mm,
 59                        regions=[geo["chassis::headlights"], 
 60                                 geo["chassis::grills"],
 61                                 geo["mirrors::cover"]]
 62                    ),
 63                    # distance-based edge refinement
 64                    fl.SnappySurfaceEdgeRefinement(
 65                        spacing=[2*fl.u.mm],
 66                        distances=[4*fl.u.mm],
 67                        included_angle=130*fl.u.deg,
 68                        min_elem=5,
 69                        regions=[geo["underbody::front_grooves"],
 70                                 geo["chassis::door_creases"],
 71                                 geo["chassis::headlights"],
 72                                 geo["chassis::grills"],
 73                                 geo["chassis::pillars"],
 74                                 geo["drivetrain::radiator"],
 75                                 geo["chassis::window_frames"]],
 76                        retain_on_smoothing=False
 77                    ),
 78                ],
 79                smooth_controls=fl.SnappySmoothControls(
 80                    lambda_factor=0.7,
 81                    mu_factor=0,
 82                    iterations=5,
 83                    included_angle=100*fl.u.deg
 84                ),
 85                # cad_is_fluid is used to indicate that the geometry is a fluid domain (contains the farield)
 86                cad_is_fluid=True,
 87                # the point inside the mesh should be "randomized" to avoid placing it on a node or an edge
 88                zones=[fl.MeshZone(point_in_mesh=[0.234, 0.18172, 2.1324]*fl.u.m, name="fluid")],
 89                snap_controls=fl.SnappySnapControls(
 90                    tolerance=2,
 91                    n_smooth_patch=3,
 92                    n_feature_snap_iter=30,
 93                    multi_region_feature_snap=True,
 94                    strict_region_snap=True
 95                ),
 96                castellated_mesh_controls=fl.SnappyCastellatedMeshControls(
 97                    resolve_feature_angle=15*fl.u.deg,
 98                    n_cells_between_levels=2,
 99                    min_refinement_cells=10
100                ),
101                quality_metrics=fl.SnappyQualityMetrics(
102                    max_concave=50*fl.u.deg,
103                    max_non_ortho=60*fl.u.deg,
104                    max_boundary_skewness=10*fl.u.deg,
105                
106                ),
107                bounding_box=fl.Box(center=[20, 0, 10]*fl.u.m, size=[130, 60, 30]*fl.u.m, name="bounding_box")
108            ),
109            # volume meshing parameters
110            volume_meshing=fl.BetaVolumeMeshingParams(
111                defaults=fl.BetaVolumeMeshingDefaults(
112                    # boundary layer settings used by default
113                    boundary_layer_first_layer_thickness=2*fl.u.mm,
114                    boundary_layer_growth_rate=1.2
115                ),
116                volume_zones=[far_field_zone],
117                refinements=[
118                    # boundary layer refinement
119                    fl.BoundaryLayer(faces=[geo["chassis*"], geo["wheel*"]], first_layer_thickness=0.5*fl.u.mm)
120                ]
121            )
122        ),
123        operating_condition=fl.AerospaceCondition(velocity_magnitude=38.8*fl.u.m /fl.u.s),
124        reference_geometry=fl.ReferenceGeometry(
125            moment_center=(1.4001, 0, -0.3176),
126            moment_length=(1, 2.7862, 1),
127            area=2.17
128        ),
129        time_stepping=fl.Steady(
130            max_steps=7000
131        ),
132        models=[
133            fl.Wall(surfaces=[
134                geo["chassis*"], 
135                geo["mirrors*"], 
136                geo["exhaust*"], 
137                geo["underbody*"],
138                geo["drivetrain*"], 
139                geo["wheel*"],
140                geo["suspension*"],
141                geo["tunnel::ground_back"]
142            ],
143            use_wall_function=True),
144            fl.SlipWall(
145                surfaces=
146                [geo["tunnel::sides"], 
147                geo["tunnel::top"], 
148                geo["tunnel::ground_front"]]
149            ),
150            fl.Freestream(surfaces=[geo["tunnel::inlet"], geo["tunnel::outlet"]]),
151            fl.Fluid(
152                turbulence_model_solver=fl.KOmegaSST(),
153                navier_stokes_solver=fl.NavierStokesSolver(
154                    low_mach_preconditioner=True
155                )
156            )
157        ],
158    )
159
160
161project.run_case(params=params, name="DrivAer_snappy_example", use_beta_mesher=True)

After running the code, the following results are expected:

../../_images/mesh.png

Fig. 3.2.6 The example base STL geometry.#

../../_images/flow.png

Fig. 3.2.7 Pressure coefficient contour.#

../../_images/flow2.png

Fig. 3.2.8 Velocity vector field on a slice and CfVec vector field on a surface.#

Translate a custom case to snappyHexMesh API#

To use a working custom snappyHexMesh case with the snappyHexMesh API the following steps can be taken to define the meshing parameters:

  1. Define default values for spacings and convert levels to dimensions with the following:

(3.2.1)#\[dim=\frac{\Delta}{2^{level}}\]

where \(\Delta\) is the background mesh size computed using the method explained in step 3 of the detailed workflow.

  1. Define body refinements with the body name in the STL file by defining a SnappyBody. Region refinements can only be used if the naming of the surfaces follows the convention ‘body::patch’ for example. Use minimum and maximum spacings from previous expression.

  2. Create surface edge refinements with a combination of parameters specified in the custom surfaceFeaturesDict and the custom snappyHexMeshDict.

  3. Smooth, snapping, castellating and quality controls can be directly translated since the names are self-explanatory.

Common issues#

  1. Overrefinement of underlying facets: snappyHexMesh may sometimes refine entire underlying STL facets even if they are not fully enclosed by the UniformRefinement region. For more details, see the Setup section.

  2. Improper geometry preparation: A lot of issues may arise due to the specific way the geometry is handled in the workflow. For specific instructions on how to prepare it refer to the Geometry preparation section.

  3. Bounding box definition: The bounding box of the geometry is sometimes improperly detected, if the meshing fails for some reason it may be effective to specify the bounding box explicitly, it cannot intersect with any geometry surfaces.