Independent · OpenFOAM v13 · compose-first
Every OpenFOAM case starts as a copied tutorial. By the fifth copy nobody can say what changed.
That is how nearly every OpenFOAM case in the world is made, this project's own included: start from the tutorial nearest the problem and edit the thirty-odd dictionaries until the solver stops complaining. It works once. It stops working when you own nine cases and want to compare them. aeroBASE keeps the tutorials as the baseline and moves the editing into four small JSON descriptors: they merge in a fixed order into one token map, and a manifest renders the case against it. The case becomes an output. What you keep and diff and review is the four files.
What an OpenFOAM case is
The problemA case is a directory. Inside it are three folders and about
thirty text files: 0/ holds one file per field with its boundary
conditions, constant/ holds the fluid properties and the turbulence model,
and system/ holds the mesh definition, the numerical schemes, the linear
solvers, the decomposition for parallel running and the run control. Every one of
them is a dictionary in OpenFOAM's own syntax, and they all have to agree with each
other or the solver either refuses to start or, worse, starts.
The normal way to make one is to copy a tutorial that is close and edit it. That works, and it is how most cases in the world are made, and it has one failure mode that gets worse with every case you own: after the fifth copy nobody can say what is different about case three. The differences are spread across thirty files, most of which were never touched, and the ones that were touched carry no record of why.
The other way is to say what you want in a form small enough to read, and generate the thirty files from it. That is what this repository is. The thing you write, keep and review is four JSON files of a few dozen lines. The thirty dictionaries are output, they live in a directory that is ignored by git, and they are meant to be thrown away and made again.
This entry and aeroAUTO take the same working rule, that a run should be reproducible from one declarative file, in two different forms. aeroBASE generalises it into an engine with a template library; aeroAUTO applies it to one physics domain, one YAML per study. Both hold an Ahmed body case and build it completely differently. Neither repository reads the other, and aeroBASE's contract names its consumers, two wing and aircraft repositories not shown here.
Four files, in a fixed order
The mergeThe four descriptors are not four halves of one file. They are four different kinds of decision, made by different people at different times, and the order they merge in is the contract the whole engine rests on. Later wins. Below is the merge running: each step adds one descriptor and the table shows what that layer put in, and what it took out of somebody else's hands.
run
Produced by running the repository's own print_context.py once per layer
during this build. It writes nothing and it does not touch a solver. The seven rows
marked in the accent are the only tokens any layer overrode.
Narration
01 What nobody edits
Policy is the layer nobody edits per run. It carries the gates every case has to clear before its answer counts: here, the residual each field must fall below. Eleven tokens, and they are the only ones in the whole map that are about trust rather than about physics. Profile is the structural shape: two or three dimensions, internal or external, compressible or not, which solver, which turbulence model, which numerical style. It is deliberately free of numbers, and it is the layer a case is named after. It brings the map from eleven tokens to forty-nine.
02 Where the numbers arrive
Study is where the numbers arrive, and where they arrive as sets: three Reynolds numbers, three angles of attack, one yaw. It also, here, relaxes two gates the policy set, loosening the pressure residual by three orders of magnitude and the velocity residual by two. That is legal, and it is exactly the kind of thing you want to be able to see. Run picks one cell out of the study's grid: one Reynolds number, one angle, one yaw. Everything else it adds is bookkeeping, an identifier, a decomposition, a write interval. Eighty-two tokens, seven of which some layer took away from an earlier one.
Seven overrides in eighty-two tokens is not much, and that is the reading: in a healthy configuration the layers mostly do not fight, because they are about different things. What matters is which seven. Two of them are the study relaxing the residual gates the policy set, from 1e-5 to 1e-2 on pressure and 1e-6 to 1e-4 on velocity. That is a real decision with real consequences for whether a converged run is converged, and in a copied tutorial it would be a line in a file nobody diffed.
Three more are the run collapsing the study's sweep to a single point: Reynolds number from three values to one, angle of attack from three to one, yaw from a one-element list to a scalar. That is the division of labour the merge order exists to express. A study says which grid of conditions is interesting; a run says which cell of it is being computed right now.
A name that says what the case is
The grammarNine cases are curated in the repository, and none of them is
called case1. Each carries an identity built from the same six fields, in
the same order, so the directory name is a readable statement of what was solved:
FAMILY _ DIMENSION _ PHYSICS _ metrics _ MODEL _ solver
so CYL_2D_INC_Re100_Lam_ico is a cylinder, in two
dimensions, incompressible, at Reynolds 100, laminar, on the PISO transient solver.
That is the whole naming scheme, and it is enforced by a schema rather than by
discipline. Sorting the directory sorts the physics.
With one exception the schema does not cover, and it is visible in the table below: the two wedge cases separate their two metrics differently, Ma2-W15 in one directory name and Ma2_W15 in the other. The fields are the same, in the same order, and the schema validates both. What it does not fix is the punctuation between them, so those two cases do not sort adjacent to each other and a reader looking for the pair has to know that.
repo
The nine, from their own case.json identity blocks. Two carry committed
numeric output and one row disagrees with itself.
which one
Nine physics, one pipeline
This is the claim the whole repository rests on, and it is the kind of claim only a grid can make. A cavity, a cylinder, a step, a plate, a bluff body, two shock problems and a wing: laminar and turbulent, incompressible and compressible, two dimensions and three, steady and transient. Nothing between them changed except the descriptors.









What came out of it
ResultsAn engine with no results is a claim. These are the three the repository committed as data rather than as a picture, and the third one is a number the case asked for and never wrote down.
A cylinder, shedding
Flow past a circular cylinder at Reynolds 100. Above about 47 the steady wake stops being steady: vortices detach alternately from each side and the wake becomes the periodic street below. Four frames from the run's own animation, evenly spaced through it.
The lid-driven cavity, down its own centreline
A square box with a lid sliding across the top and no other motion. It is the oldest test in incompressible CFD because it has no inflow, no outflow and no geometry to argue about, so any disagreement is the numerics. The line plotted is the horizontal velocity sampled down the vertical centreline, which is the profile the benchmark literature tabulates.
The lid drags the fluid across the top at unit speed, the return flow fills the lower two thirds, and the crossing where the profile passes zero is the height of the primary vortex centre. This is the raw sample the repository committed, 1001 points, not a fit through it.
The number the case asked for
The backward-facing step is the standard shakedown case for separated flow. Fluid runs along a channel, the floor drops away, the boundary layer cannot follow the corner and separates, a recirculating bubble forms in the step's shadow, and some distance downstream the shear layer comes back down and reattaches. The distance is Lr, and normalised by the step height it is the one number everybody reports.
The case's own README ends with a next-steps list, and the first item is: record Lr/h explicitly, both by the u = 0 contour and by the wall-shear sign change, and append the value here. It was never appended. The wall shear field needed for the second method is committed, so here it is.
Narration
01 What the sign means
Streamwise wall shear along the floor, from the base of the step to the end of the straight section, drawn with the sign the solver writes. That sign is the opposite of the textbook one, and it has to be said before the plot means anything: OpenFOAM's wallShearStress is the traction on the wall taken with the inward normal, so it comes out negated against the usual definition. Here, then, positive is fluid at the wall moving backwards towards the step, which is the recirculation, and negative is flow that has reattached and is heading downstream again. The crossing is the answer, and the crossing is where it is whichever way round the sign is written. Right in the step's corner there is a second, smaller change of sign. That is the corner eddy: a counter-rotating vortex tucked into the angle, spun up by the main recirculation turning against the wall. It ends at step heights, and it is a real feature of this flow rather than noise.
02 The crossing, and what it rests on
The main crossing. The shear goes positive through the whole bubble and turns negative at , which on a step is . That is the number the case's README asks for and never records. And here is what it rests on. The crossing is not measured, it is interpolated between the last face with positive shear and the first with negative, and those two faces are step heights apart. So the resolved statement is . That is a mesh spacing, not a confidence interval.
new
Computed here from post/wallShearStress_286.vtk and the case's own
blockMeshDict. The wall face positions are not stored with the field, so
they are reconstructed from the mesh: the bottom wall is 180 cells over 206 mm with a
geometric expansion ratio of four, which fixes every face centre exactly. Nothing here
is compared against a reference value, because the repository does not carry one.
Preview first, write second
The safety designA generator that writes files is a generator that can overwrite them. The whole tool surface here is built so that the destructive thing is never the default and never one keystroke away.
Print the tokens
print_context.py
Merges the descriptors and prints the token map. Touches nothing. This is what produced the table in chapter 02, and it is the first thing to run when a case is behaving oddly, because most of the time the answer is that a token is not what you thought it was.
Plan the render
plan.py
Walks the manifest and reports which templates would render, where each would
land, and which tokens are still missing. Touches nothing. A plan that reports
missing: 0 is the gate before anything is allowed to be written.
Write the workspace
compose_write.py
The only tool that can create files, and it is DRY by default: run it
plainly and it tells you what it would do. Writing needs an explicit
--no-dry, and it writes into .work/<runId>/, which
is ignored by git and expected to be deleted.
The workspace being disposable is the load-bearing part. If a generated case is precious then the generator has to be perfect, and it never is. If the generated case can be deleted and remade from four small files in a second, then the four files are the only thing that has to be right, and they are small enough to read in full before you commit them.
The curated cases in cases/ are the exception, and they are curated on
purpose: a workspace that produced something worth keeping gets promoted, with a
checklist, into a directory that is tracked. That is the one place in the repository
where generated output becomes a source artifact, and it is a deliberate act with a
script behind it rather than a directory somebody forgot to ignore.
What a written workspace contains
Three of the nine meshes, as blockMesh built them from the descriptors. A mesh is the most consequential thing the pipeline writes and the least reviewable: it is the one output where a mistake looks like a plausible answer, which is the reason the plan step reports cell counts and the promotion checklist asks for the mesh check log before a case is allowed into cases/.



Where the engine stops
LimitsNine canonical cases with known answers are a shakedown, not a validation. Everything here is about whether a case can be constructed correctly and repeatably; none of it is evidence that a result is right.
- repoWhat the repository states, in a descriptor, a README or a committed run output.
- runProduced by running one of the repository's own preview tools during this build. No writes, no solver.
- newComputed here and recorded nowhere in the repository. One thing only: the reattachment length.
What it shows
Nothing in this repository solves a new equation. What it carries is a boundary that most CFD work never draws: between the decisions that describe a problem and the thirty files that encode them. Once that boundary exists you can diff a study, review a policy, tell what is different about case three, and delete a workspace without losing anything, because the workspace was never where the work lived.
The nine cases are the evidence that the boundary holds across physics rather than for one flow: a lid-driven cavity, a cylinder, a backward-facing step, a flat plate, a cube, an Ahmed body, a wing and two shock reflections all come off the same pipeline, and the only thing that changes between them is the descriptors.
What I would do differently
Compute the metric each case exists for, at the time. The backward-facing step has one headline number, its README says so, and the field needed to get it was committed alongside a note asking for it. The gap between having the data and having the answer was a short script, and it stayed open long enough that a portfolio page is where the number finally got written down. "A case is not promoted until its own metric is in its own README" would have closed it.
An engine layer. Not validated CFD, not a solver, and not benchmarked against reference data.