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.

Object
a case builder, not a case
Contract
Policy < Profile < Study < Run
Default
preview. Writing is a flag.
01

What an OpenFOAM case is

The problem

A 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.

02

Four files, in a fixed order

The merge

The 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.

03

A name that says what the case is

The grammar

Nine 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.

A square cavity with the lid sliding across the top, coloured by velocity magnitude, with a line plot of the centreline profile beside it.
repo LDC · Lid-driven cavity. Velocity magnitude and the centreline profile the section above plots, as the run wrote them.
Alternating red and blue vortices trailing downstream of a white circular cylinder in a horizontal channel.
repo CYL · Cylinder, Re 100. The vortex street as a single frame, in the signed field rather than the magnitude the animation uses.
A horizontal channel coloured blue to green to yellow with streamlines, and a closed black contour marking a recirculation bubble along the lower wall.
repo BFS · Backward-facing step. Streamwise velocity with streamlines, and the closed recirculation bubble outlined in black below the step.
A green curve of streamwise velocity against distance from a wall, rising steeply from zero and flattening near 150.
repo PLATE · Flat plate, zero pressure gradient. The boundary layer profile: velocity against height, rising from the no-slip wall to the freestream in under a centimetre.
A dark red field with a small pale body near the lower edge and a disturbed wake trailing behind it.
repo AHMED · Ahmed body. The symmetry plane, with the wake behind the slant. The same benchmark aeroAUTO studies, run here as an exercise of the pipeline rather than as a study.
A dark red region above a wedge and a paler blue-grey region below it, separated by a perfectly straight diagonal line.
repo WEDGE · Oblique shock, Mach 2. A supersonic stream turned by a fifteen degree wedge. The straight line separating the two flat regions is the shock, and there is nothing to it but a jump.
A green field over a white wedge with a faint bright band crossing it near the top.
repo WEDGE · The same shock, reflected. The reflection case: the shock strikes the upper wall and comes back down. Two shocks, one geometry, and the reflection angle is not the incidence angle.
A rectangular domain drawn as a fine white mesh with a red inlet face, a green outlet face, a blue refinement region and a small white cube inside it.
repo CUBE · Wall-mounted cube. The mesh rather than a field, because for this case the mesh is the interesting part: a refined block around the body inside a graded background.
A square filled with fine swirling grey streaks forming one large central vortex and smaller ones in each corner.
repo LDC · The cavity again, as texture. A line-integral convolution of the same cavity field: every streak follows the local flow, so the primary vortex and the three corner eddies appear without a single streamline being placed by hand.
04

What came out of it

Results

An 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.

05

Preview first, write second

The safety design

A 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/.

A square filled with an evenly spaced square grid.
repo the cavity. A uniform grid, because the geometry has nothing to resolve except the walls.
A rectangular domain with a circular O-grid around a small cylinder near the left, the cells stretching downstream.
repo the cylinder. An O-grid wrapped around the body and stretched into the wake, because that is where the vorticity goes.
A wedge-shaped domain filled with a mesh whose lines converge toward the leading edge and toward the upper wall.
repo the reflected wedge. Graded toward the wall the shock strikes, because a shock smeared over four cells is not a shock.
06

Where the engine stops

Limits

Nine 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.