Skip to main content

Time Integration

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.

This tutorial reuses the blunted-cone hypersonic case from Getting Started, but the goal here isn't the flowfield — it's the Integration and Restart blocks, and the machinery that decides how the solver steps through time, when it stops, and how to pick a run back up. Specifically, you'll work through:

  • What CFL actually controls, and what happens when you set it too high.
  • The three mutually-exclusive ways to tell the solver when to stop: max_steps, max_time, and max_time_char.
  • What time_scale is for (and why you usually don't need to touch it).
  • The four ways to point Restart::file at a previous solution, and a gotcha that trips people up the first time they use one.

The case is otherwise identical to Getting Started, just coarsened — dxmin is 0.006 here instead of 0.001 — so that every experiment below finishes in well under a minute instead of several minutes. That's the only thing that changed in Domain; everything from Geometries through DomainBC is exactly what Getting Started already walked through, so this tutorial won't repeat that pass.

Case Summary​

QuantityValue
Freestream Mach number6
Angle of attack12.5°
Freestream pressure (P∞P_\infty)10 kPa
Freestream temperature (T∞T_\infty)100 K
Initial grid size≈\approx 6.72 million cells
Minimum grid spacing (Δx\Delta x)≈\approx 5.83 mm (target 6 mm)

Running the Case​

champs+ input.sdf

With the coarsened grid, this finishes in around 10-15 seconds on a single GPU (versus several minutes for the full-resolution Getting Started case) — you'll be re-running this a handful of times below, so it's worth confirming it completes quickly before moving on. Output goes to sim/, with the same layout as Getting Started:

sim/
log/ solver log files
restart/ binary .db checkpoint files, written every Restart::output_interval steps (100)
slice/, surface/, profile/ field outputs, written every 100 steps
_cache/, _comm/, _debug/ used internally by the solver

As shipped, input.sdf runs for max_steps = 500 at CFL = 0.45. Keep that log output around after this first run (or just note the timings) — you'll compare against it below.

The Integration Block​

Integration
{
CFL = 0.45
scheme = "ssprk3"
max_steps = 500
time_scale = Lscale / umag
}

See the Integration reference for the full set of keys. Two things are going on in this block: how big a time step to take (CFL), and when to stop (max_steps, here). We'll take each in turn, then look at time_scale.

Commenting out lines

The input file supports C-style comments — anything after // on a line is ignored. That's a convenient way to run the experiments below: comment out the original line instead of overwriting it, so you can switch back by uncommenting rather than having to remember (or re-derive) the original value.

// CFL           = 0.45
CFL = 0.8

CFL​

CFL doesn't set a time step directly — it sets a stability limit, and the solver computes dt from it using the local grid spacing and wave speed. Since neither fixed_CFL nor timestep is set in this file, the solver recomputes dt from CFL periodically as the flow develops (not literally every single step) rather than freezing it at startup. You can see this in the log — dt drifts slowly as the solution evolves:

[I] timestep (dt / dt_char): 1.75932e-06 / 0.00211648
...
[I] timestep (dt / dt_char): 1.79464e-06 / 0.00215898

CFL = 0.45 is comfortably stable for this case. Now try pushing it too far — edit Integration::CFL to 0.8 and rerun:

champs+ input.sdf

The run looks completely normal for a while — 184 steps at CFL 0.45 would already be well past halfway to max_steps, and here too each step reports fine right up until it doesn't:

[I] nt: 184       time: 0.000587365   chartime: 0.706606280
[I] nt: 185 time: 0.000590545 chartime: 0.710432744
[W] detected crash with code: irreg_points
[E] detected crash, unrecoverable!
execution terminated. error: "Edetected crash, unrecoverable! "
[wvn-gigatop:287491] *** Process received signal ***
[wvn-gigatop:287491] Signal: Aborted (6)

A couple of things are worth observing:

  • It doesn't fail immediately. At a marginally-too-high CFL, the solution can look fine for dozens or hundreds of steps before the immersed-boundary reconstruction near the cone surface finally becomes inconsistent and the solver aborts — irreg_points here means the count/placement of irregular (cut-cell) points near the IBM surface stopped making sense, which is a downstream symptom of the flowfield near the wall blowing up. Don't assume a run that survives its first few hundred steps is necessarily stable for the long haul if you've pushed CFL up.
  • Your last checkpoint survives. Since Restart::output_interval = 100, sim/restart/restart_000000100.db was already on disk before the crash at step 185 — you haven't lost the run, just the last ~85 steps. This is exactly the restart machinery covered below.

Set CFL back to 0.45 before continuing.

Finding the actual limit

For this specific case/grid, CFL up to about 0.75 runs cleanly through all 500 steps; 0.8 and above eventually crashes (sooner as you increase it further — CFL = 1.2 fails by step 2). Where that line falls depends on the grid, scheme, and flow, so treat 0.45 as a comfortable working value rather than a hard rule.

max_steps, max_time, and max_time_char​

Exactly one of these three keys must be set — they're different ways of expressing the same thing, "when do I stop":

KeyUnitsStops when...
max_stepstimestepsthe iteration counter nt reaches this value
max_timesolver-unit secondsthe physical solver time t reaches this value
max_time_charcharacteristic timest / time_scale reaches this value

As shipped, this case uses max_steps = 500, which finishes at chartime ≈ 1.07. Try swapping it for the other two and see how the stopping point shifts. Replace max_steps = 500 with:

max_time = 0.0006

Rerun, and the solver stops as soon as the physical solver-time crosses 0.0006 s — in practice that's nt = 336 (time = 0.0005997), noticeably short of the 500 steps the original run took. Now try:

max_time_char = 1.0

This stops at nt = 467 — closer to the original 500-step run (which finished around chartime = 1.07), but not identical, since dt isn't perfectly constant step to step. This is the expected behavior: max_steps, max_time, and max_time_char are three different stopping conditions, and since dt adapts as the flow develops, they won't generally land on exactly the same step count even when tuned to "the same" run length.

Set max_steps = 500 back before moving on to Restart.

time_scale​

time_scale is a single number (or expression) that defines your simulation's "characteristic time" unit. Here it's set to a convective time scale, Lscale / umag — roughly the time for the freestream flow to cross one reference length Lscale. It's consumed in exactly two places:

  1. As the divisor for max_time_char above (chartime = t / time_scale).
  2. By any IO output using the "tchar" or "relative_tchar" time-control specifiers (see the IO reference) — none of this case's outputs currently use those, but they're available.

It has no effect on the physics, the grid, or the raw dt — it's purely a reporting/thresholding convenience. You're free to tune it to whatever's intuitive for your problem (a convective time, an acoustic time, a rotation period, ...), but you don't have to: leaving it at 1.0 would run identically, just with the chartime column in the log reported in raw seconds instead of convective units. Pick whatever scale makes the numbers in your log and plots easiest to reason about.

Restarting a Simulation​

Restart
{
file = "none"
output_interval = 100
}

See the Restart reference for the full set of options. output_interval = 100 is what's been writing sim/restart/restart_00000NNNN.db checkpoints throughout every run above; file controls whether a run starts cold ("none", the default here) or picks up from one of those checkpoints. file accepts four practical forms — try each of these in turn.

Start fresh: with file = "none" and max_steps = 200, run the case. You should end up with restart_000000100.db, restart_000000200.db, and a final restart_000000201.db (the solver always writes one last checkpoint when it stops, even off-interval) in sim/restart/.

1. By timestep integer. Set:

Restart
{
file = "100"
output_interval = 100
}
Integration
{
...
max_steps = 300
}

Rerun. The log confirms it found and loaded the right file:

[I] initialize using database ".../sim/restart/restart_000000100.db"
...
[I] nt: 100 time: 0.000180543 chartime: 0.217196000
max_steps counts from the restart point, not from zero

This run doesn't stop at nt = 300 — it resumes at nt = 100 and runs 300 more steps, stopping at nt = 400 (plus one final checkpoint at 401). max_steps (and likewise max_time/max_time_char) is always relative to wherever the loaded restart left off, not an absolute target. If you want to resume up to a specific absolute step, you need to account for the offset yourself.

2. By explicit filename/path. Set file to a path directly, e.g.:

file = "sim/restart/restart_000000200.db"

This loads exactly that file regardless of what else is in sim/restart/, and resumes at nt = 200.

3. By "recent". Set:

file = "recent"

This loads whichever file in sim/restart/ is lexicographically latest by filename — with the zero-padded step numbers used here, that's the same as "most advanced," but if you ever rename or hand-copy restart files in, keep in mind it's a filename sort, not a true timestamp check. (restart.md also documents "recent_time", which instead picks the file with the newest file-modification timestamp — useful if filenames alone wouldn't disambiguate.)

4. "none". Cold start from InitialCondition, ignoring anything in sim/restart/. This is what the file ships with.

Set Restart::file back to "none" and Integration::max_steps back to 500 once you're done experimenting, to leave the case in its original state.