Skip to content

The case workflow

An urbanMicroclimateFoam case is prepared and run by the same sequence of steps every time. Cases spell the sequence out in shell scripts (Allprepare, Allrun, Allclean) that are copied from case to case and edited by hand. umcfGui reads those scripts into a workflow: the sequence with its few choices. It checks the workflow and can write the scripts again for the case's OpenFOAM version.

The pattern

Prepare (Allprepare), region by region:

Region Steps
Fluid or vegetation, meshed with snappyHexMesh surfaceFeatures (once, for all regions); blockMesh from the region's variant system/blockMeshDict.<r>; createPatch; snappyHexMesh -overwrite from system/snappyHexMeshDict.<r>; move constant/polyMesh to constant/<r>/polyMesh. Case scripts copy the variants over system/blockMeshDict; the written scripts pass them with -dict
Solid, meshed with blockMesh blockMesh -region <r> from system/<r>/blockMeshDict
Then, any region createPatch -region, changeDictionary -region, topoSet -region (setSet -batch in OpenFOAM-8), and setFields -region after restoring the field it sets from its backup (0/air.bckp/LAD)

Run (Allrun): decompose every region; copy the region dictionaries into each processor directory; the radiation pre-processing of the vegetation region (faceAgglomerate, calcLAI on the air, viewFactorsGen, solarRayTracingGen); the solver; reconstruct every region.

Clean (Allclean): cleanCase, the region meshes, and the radiation pre-processing outputs of a serial run.

Reading a case's scripts

The scripts are read without running anything: commands, for loops over words, variables set to words, if blocks (their steps may not run), mpirun/srun, #SBATCH/#PBS lines, and the OpenFOAM RunFunctions and CleanFunctions helpers. A word that can't be known without running the script ($(…), a variable set from a command, a cd elsewhere) is kept and marked; the check doesn't guess it.

Scripts are case files, read every time a case opens, so their reading is bounded: blocks nested more than 32 deep are not read, and a script stops being read after a million commands (loops count each pass) or 100 000 steps. Such a script gets a note, and is checked only as far as it was read.

Steps that don't fit the pattern are listed (workflow.custom), as are steps that fit but aren't needed (workflow.not-needed, for example a serial faceAgglomerate before the parallel one).

Writing the scripts

The scripts umcfGui writes follow the pattern, with what each OpenFOAM version needs:

OpenFOAM-8 OpenFOAM-12
Sets in solid regions setSet -batch system/<r>/setset.batch topoSet -region <r> (system/<r>/topoSetDict): OpenFOAM-12 has no setSet
faceAgglomerate dictionary constant/<r>/viewFactorsDict system/<r>/viewFactorsDict, passed with -dict (on its own it reads system/viewFactorsDict)
viewFactorsGen, solarRayTracingGen constant/<r>/viewFactorsDict system/<r>/viewFactorsDict

Also, in both versions: - regions are decomposed and reconstructed one by one with -region, so OpenFOAM-12 needs no regionSolvers in controlDict; - no process count is written into the scripts. OpenFOAM-12 parallel steps use runParallel, which takes it from system/decomposeParDict. OpenFOAM-8 decomposes each region by its own system/<r>/decomposeParDict, and a case may have no system/decomposeParDict (which runParallel always reads): there the count is read from the first region's and the steps call mpirun themselves; - each step logs to log.<application>.<region> and overwrites its log (with runApplication a step whose log exists is otherwise skipped, silently); - the scripts stop at the first step that fails; - the region dictionaries are copied into each processor directory. urbanMicroclimateFoam built before June 2026 (solver commit 40f31d4) looks for vegetationProperties and the like in processor*/constant/<region>/; without them it builds no vegetation model and stops at the first time step (request for volScalarField Tl from objectRegistry air failed). Later builds read them from constant/. The copies work with both; - Allclean removes the files exactly, by name (no F*), and only the meshes Allprepare builds; - names and paths are quoted for the shell when they need it. Since they come from the case, no script is written with a region, field or file name that isn't a plain name (/, .., a control character, a leading -) or a dictionary path outside the case. A region directory that is a symbolic link out of the case is not caught: the scripts' rm and mv would follow it.

A mesh built in constant/polyMesh keeps its steps in the order the case's script ran them (createPatch before or after snappyHexMesh); a variant copied into system/ or passed with -dict is the same dictionary. A region step with its own -dict, or a field restored without a setFields after it, doesn't fit and is listed.

Run on the 3×3 test case (OpenFOAM-12, two processes), the written scripts mesh, pre-process and run the case through to its first written time.

For now this is the workflow example of the umcf-model crate: cargo run -p umcf-model --example workflow -- <case> [--write] prints the scripts, or with --write replaces the case's, writing each atomically; the old ones are kept as <name>.bak, then <name>.1.bak, <name>.2.bak, … (a backup is never overwritten).

The workflow description

The workflow is also kept in the case itself, as system/umcfWorkflow: an OpenFOAM-style dictionary that names each region with its kind, how its mesh is built and the steps after it, each patch with its role and partner, the radiation pre-processing and the solver. It is the first piece of the common workflow; the scripts and dictionaries are to be written from it.

version     12;

regions
{
    air
    {
        kind        fluid;
        mesh        ( blockMesh "system/blockMeshDict.air" createPatch snappyHexMesh "system/snappyHexMeshDict.air" );
        steps       ( changeDictionary topoSet setFields ( ( "0/air.bckp/LAD" LAD ) ) );
        patches
        {
            west            { role inlet; }
            street          { role coupled; partner ( street street ); }
        }
    }
    street
    {
        kind        solid;
        mesh        blocks;
        steps       ( createPatch changeDictionary topoSet );
        patches
        {
            street          { role coupled; partner ( air street ); }
            interior        { role interior; }
        }
    }
}

features    no;
radiation   { region vegetation; lai air; viewFactors yes; solarRayTracing yes; }
solver      urbanMicroclimateFoam;
reconstruct yes;
Key What
version 8 or 12: the OpenFOAM version the scripts are for
kind fluid, solid or vegetation
mesh blocks (blockMesh -region), or the steps that build it in constant/polyMesh before it is moved to the region: blockMesh, createPatch, snappyHexMesh, each meshing step with its dictionary when it isn't the default one
steps after the mesh: createPatch, changeDictionary, topoSet (or setSet "batch" in OpenFOAM-8), setFields with the fields it restores from their backups first
role inlet, outlet, side, top, ground, coupled, canopy, interior, bottom, sky
partner the region and patch a coupled patch is mapped to
radiation the region the view factors and solar ray tracing are computed on, and the one calcLAI runs on

workflow.import writes it from the case: the scripts read as above, the region kinds, and each patch's role. Coupled patches and their partners come from the mesh (neighbourRegion/neighbourPatch, or sampleRegion/samplePatch in OpenFOAM-8); the canopy is the vegetation patch that maps to the air's cells; the air's inlet, outlet and sides come from the patch's own velocity condition, not one given by a group or a pattern (atmBoundaryLayerInletVelocity, inletOutlet, slip); top, ground, interior, bottom and sides from their names; the vegetation region's patches take their air twin's role, the open sides and the top becoming sky. What it can't tell is listed and left without a role. On the 3×3 test case every patch gets its role. A region split from another (the street canyon's vegetation) is not described yet: its steps are listed.

workflow.show returns the description, read from the file or, when the case has none, as the import would write it; it also says when a file is there that doesn't load (not a dictionary, a directory, or over the size limit), or one the open case doesn't hold (written or undone since it was opened). workflow.import writes over neither, nor a symbolic link (to a dictionary, a field or nothing: the save would write the file it points to); with replace it writes over the case's own, whose comments and hand edits are not kept (edit.undo brings them back). In the desktop app, both commands show in the results card the problems and the import's notes first, then the settings (version, radiation, solver), then the regions with their meshes, steps and patch roles. The case check reads the file against the case (workflow.description): what can't be read in it, regions and patches the case doesn't have or it leaves out, other kinds, partners that aren't there, and another OpenFOAM version.