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:
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.
Fig. 3.2.1 The background mesh generated by blockMesh.#
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.
Fig. 3.2.2 The refined background mesh based on the mesher settings..#
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.
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:
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.
Run snappyHexMesh
foamToSurface: extract the surface mesh from the volume mesh surfaces.
Smooth: apply lambdaMuSmoothing on the resultant mesh.
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:
Default: default minimum spacing, maximum spacing and gap resolution.
Quality Metrics: values which are written to the meshQualityDict and inputs are directly parsed to the file.
Snap controls: parameters which define the snapping controls as defined in snappyHexMeshDict.
Castellated mesh controls: parameters which define the snapping controls as defined in snappyHexMeshDict.
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.
Bounding box: manually set the bounding box if not computed automatically.
Zones: list of mesh zones which will be kept (defined by the points inside of those zones).
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.
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:

Fig. 3.2.6 The example base STL geometry.#
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:
Define default values for spacings and convert levels to dimensions with the following:
where \(\Delta\) is the background mesh size computed using the method explained in step 3 of the detailed workflow.
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.
Create surface edge refinements with a combination of parameters specified in the custom surfaceFeaturesDict and the custom snappyHexMeshDict.
Smooth, snapping, castellating and quality controls can be directly translated since the names are self-explanatory.
Common issues#
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.
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.
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.