Skip to main content

Mesh Generation

Last updated: September 7, 2026

Files​

FileDescription
input.sdfInput file
cone.vtkTriangulation for the blunt cone geometry
Configuration — blunted cone
Figure 1.

Geometry of the blunted cone forebody used in this case — the same case as the Getting Started tutorial.

Introduction​

Installation required

This tutorial assumes you've followed the installation instructions, and that you've already been through Getting Started.

Generally, CHAMPS+ has been developed with a focus on automated mesh generation, minimizing the setup required to get a simulation running. That said, some cases will inevitably require more flexible meshing control, so this tutorial walks through the options available under Domain::Grid: per-direction spacing and static refinement zones tied to surface patches or geometric regions.

Case Summary​

QuantityValue
Freestream Mach number6
Angle of attack12.5°
Freestream pressure (P∞P_\infty)10 kPa
Freestream temperature (T∞T_\infty)100 K
This tutorial doesn't run the case

Integration::max_steps is set to 0 in input.sdf. The focus here is purely on the generated mesh, so the solver initializes the grid and exits immediately, without advancing the solution.

Trimmed relative to previous tutorials

This tutorial's input.sdf strips out everything not needed to generate a mesh: Domain::Grid::Refinement (the Spacing/RefinementZones/AMR block) is gone entirely, the base grid is defined explicitly via num_blocks/block_cells instead of dx_target/dx_tolerance, and the IO block has been removed since there's no solution to output. We'll build these back up piece by piece over the course of this tutorial.

Generating the Initial Mesh​

champs+ input.sdf

With max_steps = 0, this finishes almost immediately — the solver builds the grid and the IBM surface representation, then exits without taking a single step. Output goes to sim/, but the piece you care about for this tutorial lives under sim/_debug/:

sim/
_debug/ grid.main.vtk (the generated Cartesian mesh) and cone.vtk (the surface geometry)
_cache/, _comm/, log/, restart/ used internally by the solver / not relevant here

grid.main.vtk only contains the blocks, not the individual cells within them — each block is drawn as a single cube regardless of its block_cells resolution.

Viewing the mesh

Open both sim/_debug/grid.main.vtk and sim/_debug/cone.vtk together in ParaView, VisIt, or your viewer of choice — you need both loaded at once to see the generated grid alongside the surface it was built around. This is what you'll re-check after every change in this tutorial.

Building the mesh happens in three stages:

  1. Bounding box — Domain::Grid::bounds sets the outer extents of the domain, [xmin, xmax, ymin, ymax, zmin, zmax].
  2. Initial configuration — that box is divided into a lattice of blocks (num_blocks), and each block is further divided into cells (block_cells), giving the base, unrefined Cartesian grid.
  3. Refinement — static and adaptive refinement then locally subdivide that base grid near geometry or flow features.

This tutorial's input.sdf is a starting point for the rest of this walkthrough, defining stage 2 in its most explicit form:

Grid
{
num_blocks = [10, 10, 10]
block_cells = [12, 12, 12]
bounds = [-0.9, 1.9, -1.3, 1.3, 0.0, 1.3]
exchange_cells = [2, 2, 2]
exchange_order = 0
}
  • bounds is the bounding box from stage 1.
  • num_blocks = [10, 10, 10] and block_cells = [12, 12, 12] together define the initial configuration: a 10×10×10 lattice of blocks, each holding 12×12×12 cells. Each entry of block_cells must be divisible by 4.
Initial block lattice with the cone geometry
Figure 2.

The initial mesh from sim/_debug/grid.main.vtk: a 10×10×10 lattice of blocks spanning bounds, shown here at block resolution (each block's 12×12×12 cells aren't individually drawn), with the cone surface from cone.vtk embedded but not yet resolved by any refinement.

Try it yourself

Try changing the num_blocks values and rerunning to see how the initial configuration changes.

Refining Around a Geometry​

Add a Refinement block to start stage 3 — it nests inside Grid, as a sibling of num_blocks/block_cells/bounds, not as its own top-level block:

Grid
{
num_blocks = [10, 10, 10]
block_cells = [12, 12, 12]
bounds = [-0.9, 1.9, -1.3, 1.3, 0.0, 1.3]
exchange_cells = [2, 2, 2]
exchange_order = 0
Refinement
{
Spacing
{
surface_spacing = [0.002, 0.002, 0.002]
}
RefinementZones
{
surface_refine
{
specifier = "surface_component"
spacing = "surface_spacing"
distance = 0.05
}
}
}
}
  • Spacing isn't a refinement rule by itself — it's just a place to name target resolutions (surface_spacing here) once, so they can be shared by name across RefinementZones and, later, AMR, rather than repeating the same numbers in each one.
  • surface_refine is a RefinementZones entry — a surface_component zone, set by its specifier. The name surface_refine itself is arbitrary, just like surface_spacing above: it doesn't select any particular behavior, it's simply a label for this zone that shows up in log output. Call it whatever's descriptive to you.
  • This zone refines any block within distance of the geometry surface down to surface_spacing. Neither geometry nor component is set here, so both default to "any" — this refines around any geometry in the domain, not just cone.
z=0 cross-section with distance = 0.05
z=0 cross-section with distance = 0.25
Figure 3.

A z = 0 cross-section of sim/_debug/grid.main.vtk after adding surface_refine, with distance = 0.05 (left) versus distance = 0.25 (right): the refined region grows with distance, and at 0.25 it's already swallowing a large fraction of the domain.

Keep distance small

As distance grows, the number of blocks refined to surface_spacing grows with it — well past the region actually near the geometry, as seen above. A large distance can blow up the total cell count (and the time to generate and load the mesh) far more than it looks like it should from the number alone. Keep distance modest for the rest of this tutorial; 0.05, as used above, is a reasonable starting point.

Automatically Determining Block and Cell Counts​

Picking num_blocks and block_cells by hand, as done so far, is unintuitive: there's no direct way to reason from "I want roughly 2 mm cells near the cone" to "10 blocks of 12 cells each." dx_target/dx_tolerance is an alternative way of defining the base grid from stage 2 that works backwards from num_blocks/block_cells — instead of picking a block layout and living with whatever spacing falls out of it, you give the solver a target spacing, and it automatically computes the num_blocks/block_cells layout for you. input.sdf now defines the base grid this way:

Grid
{
// num_blocks = [10, 10, 10]
// block_cells = [12, 12, 12]

dx_target = [0.002, 0.002, 0.002]
dx_tolerance = 0.05

bounds = [-0.9, 1.9, -1.3, 1.3, 0.0, 1.3]
exchange_cells = [2, 2, 2]
exchange_order = 0
Refinement
{
...
}
}

Beyond just being more convenient, letting the solver pick the block/cell counts also avoids a real pitfall of guessing them by hand. Refinement only refines until the grid is at least as fine as the requested spacing — it never leaves a block coarser than what you asked for, but because it works by repeatedly halving (an octree: each refinement step cuts a block's spacing exactly in two), it can easily overshoot and land considerably finer than requested if the base spacing doesn't divide down to your target evenly. That's exactly what happened with the hand-picked num_blocks = [10, 10, 10]/block_cells = [12, 12, 12] base grid: requesting surface_spacing = [0.002, 0.002, 0.002] against it, the solver log reported

>> dx (min):   [0.00145833, 0.00135417, 0.00135417]

— 27-32% finer than the 0.002 requested across the three directions, meaning a lot more cells near the surface than actually asked for. With dx_target/dx_tolerance computing the base grid instead, the same surface_spacing = [0.002, 0.002, 0.002] refinement zone lands much closer to the target:

>> dx (min):   [0.00198864, 0.00193452, 0.00193452]

— within the dx_tolerance = 0.05 band, rather than overshooting by 30% or more. That overshoot isn't free: the hand-picked num_blocks/block_cells grid comes out to 13,305,600 cells, versus 6,465,024 for the dx_target/dx_tolerance version — over twice as many cells to resolve a spacing nobody asked for. Prefer dx_target/dx_tolerance over hand-picking num_blocks/block_cells whenever you have a target resolution in mind, which is most of the time.

Single-parameter refinement

Add a dx_surf entry to Dictionary, and use it for both Grid::dx_target and Refinement::Spacing::surface_spacing:

Dictionary
{
...
dx_surf = 0.002
}
dx_target      = [dx_surf, dx_surf, dx_surf]
Spacing
{
surface_spacing = [dx_surf, dx_surf, dx_surf]
}

Since the base grid's target spacing and the refinement zone's target spacing are both driven by the same dx_surf, changing one number controls the resolution of the whole mesh — base grid and surface refinement together — rather than having to update dx_target and surface_spacing separately and keep them in sync by hand.

Refining Around a Specific Patch​

surface_refine targets the whole geometry (component = "any"), but a surface_component zone can just as easily target a single surface component instead, by setting component to that component's identifier. cone.vtk is made up of 5 surface components, numbered 0-4:

Surface component 0
Component 0
Surface component 1
Component 1 (rear face)
Surface component 2
Component 2
Surface component 3
Component 3
Surface component 4
Component 4
Figure 4.

The 5 surface components of cone.vtk, numbered 0-4, each highlighted individually.

Add a second Spacing entry and a second RefinementZones entry alongside surface_refine:

Refinement
{
Spacing
{
surface_spacing = [dx_surf, dx_surf, dx_surf]
patch_spacing = [dx_surf/2, dx_surf/2, dx_surf/2]
}
RefinementZones
{
surface_refine
{
specifier = "surface_component"
spacing = "surface_spacing"
distance = 0.15
}
patch_refine
{
specifier = "surface_component"
spacing = "patch_spacing"
component = 4
distance = 0.05
}
}
}
  • patch_spacing is twice as fine as surface_spacing (dx_surf/2 vs. dx_surf) — a second named resolution, defined the same way as surface_spacing back in Refining Around a Geometry.
  • patch_refine is another surface_component zone, just like surface_refine, but with component = 4 instead of the default "any" — it only refines blocks near component 4 (down to the finer patch_spacing), leaving the rest of the surface untouched by this zone. RefinementZones entries are additive: surface_refine still applies everywhere, and patch_refine layers extra refinement on top of it near component 4.
Component identifiers are strings

component always takes a string, never a number — it just so happens that cone.vtk, a VTK file, has its surface components labeled "0"-"4" by the loader, which makes component = 4 above look numeric. That's specific to VTK, not a general rule: a CGNS file, for instance, has named zones, so the equivalent geometry would have components like component = "nose_cap" or whatever that file's zones are actually called. Always check what a given geometry file's components are actually named — see the Supported Geometry File Formats reference — rather than assuming small integers.

Refinement cycling through each surface component
Figure 5.

patch_refine with component cycled through each of the 5 components in turn: the extra pocket of refinement (darkest red) tracks whichever component is currently targeted, layered on top of the surface_refine band from before.

Beyond a single component's own identifier, component also accepts a name from a Patches block — an optional convenience for grouping and naming several components together, defined under Geometries, not Grid. Getting Started already grouped components "0" and "4" together under the name nose; add that same Patches block here and reference it by name instead:

Geometries
{
cone
{
is_external = true
filename = "cone.vtk"
Patches
{
nose = ["0", "4"]
}
Transforms
{
...
}
}
}
patch_refine
{
specifier = "surface_component"
spacing = "patch_spacing"
component = nose
distance = 0.05
}
Refinement around the grouped nose patch
Figure 6.

patch_refine with component = nose: the extra refinement now covers both components 0 and 4 together — the same grouped nose region highlighted back in Getting Started — rather than either one individually.

Refining a Geometric Region​

Every zone so far has been a surface_component zone — refining relative to a geometry's surface. Not every refinement need is tied to a surface, though: a sphere zone instead refines any block that intersects a sphere you define directly, independent of where any geometry is. Add one to RefinementZones:

sphere_refine
{
specifier = "sphere"
spacing = "patch_spacing"
center = [0.3, 0, 0]
radius = 0.2
}
  • center and radius define the sphere directly, in the same solver units as everything else in Domain — unlike surface_component, distance plays no part here by default, because the sphere itself already defines the refined region rather than a distance from a surface.
  • This particular sphere is centered at [0.3, 0, 0] with radius = 0.2, which happens to land near the aft (rear) end of the cone rather than the nose — refinement zones don't need to be anywhere near the other zones already defined, or even near the geometry at all.
Refinement in a spherical region near the rear of the cone
Figure 7.

sphere_refine refines any block intersecting the sphere down to patch_spacing, regardless of proximity to cone's surface — here that lands over the aft end of the body, alongside the unrelated surface_refine/patch_refine zones still active near the nose.

Every specifier shares a couple of common fields beyond its own (center/radius here, component/geometry for surface_component, and so on): on_surface and distance. So far on_surface has been left at its default — false for sphere (it's forced true for surface_component, which is why surface_refine and patch_refine needed distance from the start). Set both explicitly to restrict sphere_refine to just the part of the sphere that's actually near a surface:

sphere_refine
{
specifier = "sphere"
spacing = "patch_spacing"
center = [0.3, 0, 0]
radius = 0.2
on_surface = true
distance = 0.05
}
Refinement restricted to the surface within the sphere
Figure 8.

The same sphere with on_surface = true and distance = 0.05: refinement is now a thin shell hugging the cone's surface within the sphere, instead of filling the sphere's whole interior as in the previous figure.

on_surface and distance aren't specific to sphere — they, along with every other field covered in this tutorial, apply the same way across all RefinementZones specifiers (box, cylinder, triangulation, and more). See the Grid Generation reference for the complete list of specifiers and fields, including ones not covered here.

Summary​

Each step below is the relevant snippet of input.sdf right after that change — use it to get back to a known-good state if you've lost track of where an edit left things.

After Generating the Initial Mesh — the starting point​

Grid
{
num_blocks = [10, 10, 10]
block_cells = [12, 12, 12]
bounds = [-0.9, 1.9, -1.3, 1.3, 0.0, 1.3]
exchange_cells = [2, 2, 2]
exchange_order = 0
}

After Refining Around a Geometry​

Grid
{
num_blocks = [10, 10, 10]
block_cells = [12, 12, 12]
bounds = [-0.9, 1.9, -1.3, 1.3, 0.0, 1.3]
exchange_cells = [2, 2, 2]
exchange_order = 0
Refinement
{
Spacing
{
surface_spacing = [0.002, 0.002, 0.002]
}
RefinementZones
{
surface_refine
{
specifier = "surface_component"
spacing = "surface_spacing"
distance = 0.05
}
}
}
}

After Automatically Determining Block and Cell Counts​

Grid
{
// num_blocks = [10, 10, 10]
// block_cells = [12, 12, 12]

dx_target = [0.002, 0.002, 0.002]
dx_tolerance = 0.05

bounds = [-0.9, 1.9, -1.3, 1.3, 0.0, 1.3]
exchange_cells = [2, 2, 2]
exchange_order = 0
Refinement
{
Spacing
{
surface_spacing = [0.002, 0.002, 0.002]
}
RefinementZones
{
surface_refine
{
specifier = "surface_component"
spacing = "surface_spacing"
distance = 0.05
}
}
}
}

After the single-parameter refinement tip (dx_surf)​

Dictionary
{
...
dx_surf = 0.002
}
Grid
{
// num_blocks = [10, 10, 10]
// block_cells = [12, 12, 12]

dx_target = [dx_surf, dx_surf, dx_surf]
dx_tolerance = 0.05

bounds = [-0.9, 1.9, -1.3, 1.3, 0.0, 1.3]
exchange_cells = [2, 2, 2]
exchange_order = 0
Refinement
{
Spacing
{
surface_spacing = [dx_surf, dx_surf, dx_surf]
}
RefinementZones
{
surface_refine
{
specifier = "surface_component"
spacing = "surface_spacing"
distance = 0.05
}
}
}
}

After Refining Around a Specific Patch (before grouping into nose)​

Refinement
{
Spacing
{
surface_spacing = [dx_surf, dx_surf, dx_surf]
patch_spacing = [dx_surf/2, dx_surf/2, dx_surf/2]
}
RefinementZones
{
surface_refine
{
specifier = "surface_component"
spacing = "surface_spacing"
distance = 0.15
}
patch_refine
{
specifier = "surface_component"
spacing = "patch_spacing"
component = 4
distance = 0.05
}
}
}

Note surface_refine's distance moved from 0.05 to 0.15 at this point in the tutorial — a deliberate change for the demos from here on, not a typo.

After grouping components into nose via Patches​

Geometries
{
cone
{
is_external = true
filename = "cone.vtk"
Patches
{
nose = ["0", "4"]
}
Transforms
{
scale
{
specifier = "scale"
scales = [0.001, 0.001, 0.001]
}
}
}
}
patch_refine
{
specifier = "surface_component"
spacing = "patch_spacing"
component = nose
distance = 0.05
}

After Refining a Geometric Region (sphere_refine added)​

sphere_refine
{
specifier = "sphere"
spacing = "patch_spacing"
center = [0.3, 0, 0]
radius = 0.2
}

Final state — sphere_refine restricted to the surface​

sphere_refine
{
specifier = "sphere"
spacing = "patch_spacing"
center = [0.3, 0, 0]
radius = 0.2
on_surface = true
distance = 0.05
}

This is the same sphere_refine shown at the end of Refining a Geometric Region, and matches the RefinementZones entry currently in input.sdf.