Skip to content

The watcher and early stopping

While a step runs, a watcher samples it every few seconds: every 5 while a solver runs, every 10 while meshing. It reads the tail of the step’s log (64 KB is enough for the last complete iteration), the whole coefficient.dat, and for snappyHexMesh the phase lines and cell counts. From two samples it gets an iteration rate and an ETA. It writes job.json only when something changed, and emits solver.progress every hundred iterations so the event log stays readable.

None of this needs the case to cooperate beyond what a forces case already has. The one addition at submit is a solverInfo function object, so residuals land in a file as well as the log.

OpenFOAM can stop itself: the runTimeControl function object has an averageCondition. Reading its source showed two properties that make it the wrong default for force coefficients.

  • Conditions combine with OR. Several conditions in one runTimeControl stop the run when any one of them is met, unless every condition carries the same groupID. Easy to miss, and the result is a run stopped on Cd while Cl is still drifting.
  • It compares the windowed mean with the current value. The test is |mean - current| <= tolerance, every iteration. A coefficient that oscillates crosses its own mean twice per cycle, so the condition fires at a zero crossing of the oscillation, not when the oscillation dies away.

The watcher’s rule looks at the whole window instead: the spread, maximum minus minimum, of each watched coefficient over the trailing 200 iterations. Cd and Cl must have a spread under 0.3% of their mean. CmPitch uses an absolute limit of 0.0002, because a moment whose mean sits near zero makes any relative test meaningless. Nothing stops before iteration 400, so the start-up transient cannot pass for convergence, and the window needs at least ten samples.

An oscillating case never satisfies a spread rule, which is the point: it should not be stopped early at all. Near-stall angles and sideslip cases run with stop: "none".

When the rule is met, the watcher rewrites endTime in the case’s controlDict, and OpenFOAM’s runTimeModifiable picks it up. Three details made that reliable.

  • The file’s timestamp has to move far enough. OpenFOAM ignores a rewritten dictionary unless its modification time is newer than the old one by more than fileModificationSkew, five seconds by default. A rewrite within the same second is invisible. The watcher bumps the timestamp past the skew after writing.
  • The edit is atomic. The dictionary is edited in a copy and moved into place, so the solver never reads half a file. Only the endTime line changes; every other line stays byte-identical.
  • End-of-run samplers have to move too. Samplers that start at a fixed iteration, such as a wall-shear sampler set to cover the last 200 of a 1000-iteration run, would never run if the case stopped at 620. With tail_iterations and retime_function_objects, the watcher moves their timeStart along with endTime.

In a sweep the watcher also manages the hand-over between points. Near a point’s end it sets stopAt writeNow, so the solver writes its fields at the point’s last iteration, and the next point’s changeDictionary starts from them. Each point’s window statistics are computed on its own iterations only.