OpenFOAM toolkit¶
Overall design¶
The OpenFOAM toolkit uses composition over inheritance — OFToolkit contains solver extensions, preprocessing objects, and post-processing pipelines as pluggable attributes rather than subclasses.
OFToolkit (hermesWorkflowToolkit)
├── OFObjectHome — field definitions and creation
├── Analysis — VTK post-processing pipeline
├── Presentation — output formats (CSV, VTK)
├── Solver extensions (composition):
│ ├── buoyantReactingFoam_toolkitExtension (attribute buoyantReactingFoam)
│ └── StochasticLagrangianSolver_toolkitExtension (attribute stochasticLagrangian)
└── Workflows (Hermes-integrated):
├── workflow_Eulerian
│ ├── workflow_simpleFoam
│ ├── workflow_buoyantReactingFoam
│ ├── workflow_scalarTransportFoam
│ ├── workflow_indoorFOAMBoussinesq
│ └── workflow_homogenousWindLogProfile
└── workflow_Lagrangian
└── workflow_StochasticLagrangianSolver
OFToolkit (toolkit.py)¶
The main class inherits from hermesWorkflowToolkit for workflow support and composes all other components. It is registered as toolkitHome.SIMULATIONS_OPENFOAM ("OpenFOAM") and passes toolkitName="OFworkflowToolkit" to the base class.
simpleFoam_toolkitExtension (eulerian/simpleFoam.py) exists but is not composed into OFToolkit; instantiate it directly with the toolkit if needed. OFLSMToolkit is a separate toolkit (registry name OF_LSM), not an attribute.
| Attribute | Type | Purpose |
|---|---|---|
OFObjectHome |
OFObjectHome |
Field definitions catalog and field creation |
analysis |
Analysis |
VTK pipeline and data extraction |
presentation |
Presentation |
Output to CSV, VTK; parallel Lagrangian loading |
stochasticLagrangian |
StochasticLagrangianSolver_toolkitExtension |
Lagrangian particle tracking |
buoyantReactingFoam |
buoyantReactingFoam_toolkitExtension |
Compressible reactive solver |
Flow type constants:
| Constant | Value | Used for |
|---|---|---|
FLOWTYPE_INCOMPRESSIBLE |
"incompressible" |
simpleFoam, wind profile simulations |
FLOWTYPE_COMPRESSIBLE |
"compressible" |
buoyantReactingFoam |
FLOWTYPE_DISPERSION |
"dispersion" |
Lagrangian particle dispersion |
Other class constants (defined in hera/simulations/openFoam/__init__.py and re-exported on OFToolkit): FIELDTYPE_SCALAR/VECTOR/TENSOR, CASETYPE_DECOMPOSED ("Decomposed Case"), CASETYPE_RECONSTRUCTED ("Reconstructed Case"), TIME_STEADYSTATE / TIME_DYNAMIC, DOCTYPE_OF_FLOWDISPERSION = "flowDispersion".
Key methods:
| Method | Description |
|---|---|
createEmptyCase(caseDirectory, fieldList, flowType, additionalFieldsDescription=dict()) |
Initialize OpenFOAM case directory structure |
writeEmptyField(fieldName, flowType, caseDirectory, timeOrLocation=0, readBoundaryFromCase=False, writeProcBoundary=False) |
Create empty field files with correct dimensions |
getMesh(caseDirectory, readParallel=True, time=0) |
Read cell centers (runs foamJob ... postProcess -func writeCellCentres) |
getMeshFromName(nameOrWorkflowFileOrJSONOrResource, ...) |
Same, resolving the case from a workflow name/document |
getMeshExtent(caseDirectory) |
Get bounding box from constant/polyMesh/points |
runOFSimulation(nameOrWorkflowFileOrJSONOrResource, scheduler="local", schedulerHost=None, schedulerPort=None, dispatch_id=None) |
Build and execute full simulation via workflow |
prepareSlurmWorkflowExecution(baseConfiguration, jsonVariations, slurmExecutionFileName="submit_all.sh", caseListFileName="cases.txt", ...) |
Generate Slurm batch submission scripts |
getTimeList(nameOrWorkflowFileOrJSONOrResourceorDirectory, singleProcessor=False, returnFirst=True) |
Extract computed time steps from case |
datasetToCaseFields(...), netcdfToCaseFields(...), datasetToSetFieldsDict(...), netcdfToSetFieldsDict(...), xarrayToSetFieldsDictDomain(...) |
Map xarray/netCDF data onto case fields or a setFieldsDict |
getVTKPipelineCacheDocuments(...) / getVTKPipelineCacheTable(...) / clearVTKPipelineCache(...) |
Query or clear cached VTK filter results (filter by regularMesh, filterName, workflowName, groupName) |
Nested classes:
| Class | Methods | Purpose |
|---|---|---|
Analysis |
getVTKPipeline(), executeVTKFiltersAndLoad(), getFiltersDocuments() |
Create and run VTK filter pipelines |
Presentation |
to_paraview_CSV(), toUnstructuredVTK(), toStructuredVTK(), loadLagrangianDataParallel() |
Export results in various formats |
Workflow system (OFWorkflow.py)¶
Workflows define the steps to build and run an OpenFOAM case. They integrate with the Hermes framework.
Workflow hierarchy:
| Class | Inherits from | Nodes |
|---|---|---|
abstractWorkflow |
hermes.workflow |
Node accessors controlDict (ControlDict node), fvSolution, fvScheme, fileWriter, parameters (Parameters node), buildAllRun, defineNewBoundaryConditions; workflowGroup / workflowGroupID |
workflow_Eulerian |
abstractWorkflow |
+ blockMesh, optional snappyHexMesh |
workflow_Lagrangian |
abstractWorkflow |
Base for particle simulations |
workflow_simpleFoam |
workflow_Eulerian |
Steady incompressible |
workflow_buoyantReactingFoam |
workflow_Eulerian |
Compressible reacting |
workflow_scalarTransportFoam |
workflow_Eulerian |
Scalar transport |
workflow_indoorFOAMBoussinesq |
workflow_Eulerian |
Indoor Boussinesq approximation |
workflow_homogenousWindLogProfile |
workflow_Eulerian |
Log-law wind profile |
workflow_StochasticLagrangianSolver |
workflow_Lagrangian |
Stochastic Lagrangian dispersion; properties dispersionName, originalFlowFieldName, dispersionFlowFieldName, dispersionDuration |
workflow_Eulerian key methods:
| Method | Description |
|---|---|
prepareICandBC(configurationFile) |
Set initial and boundary conditions from config |
set_blockMesh_blockBoundaries(minx, maxx, miny, maxy, minz, maxz, dx, dy, dz) |
Configure block mesh vertices and boundaries |
set_blockMesh_blockHeight(Z, dz) |
Adjust domain height |
ICandBCHandler_node(icnode) |
Process IC/BC node entries |
foam_snappyhexmesh_addobject(dataOrFile, objectFile) |
Add a geometry object to snappyHexMesh |
Eulerian solver extensions (eulerian/)¶
Solver extensions handle solver-specific case setup. Only buoyantReactingFoam_toolkitExtension is composed into OFToolkit (as buoyantReactingFoam).
| Class | Solver | Properties |
|---|---|---|
absractEulerianSolver_toolkitExtension |
(base) | solverName, incompressible, flowType, analysis, presentation |
simpleFoam_toolkitExtension |
simpleFOAM |
incompressible=True; adds buildSpecializedField(workflow, turbulenceType) |
buoyantReactingFoam_toolkitExtension |
buoyantReactingFoam |
incompressible=False |
Key methods on the abstract solver:
| Method | Description |
|---|---|
blockMesh_setBoundFromBounds(eulerianWF, minx, maxx, miny, maxy, minz, maxz, dx, dy, dz) |
Configure mesh boundaries from coordinates |
blockMesh_setBoundFromFile(eulerianWorkFlow, fileName, dx, dy, dz) |
Configure mesh from OBJ geometry file |
blockMesh_setDomainHeight(eulerianWorkFlow, Z, dz) |
Adjust vertical extent |
writeFieldInCase(fieldName, caseDirectory, components, xarrayData, boundaryConditions=None, data=None) |
Write a field file to the case |
IC_getHydrostaticPressure(caseDirectory, fieldName="p", groundPressure=101000) |
Generate hydrostatic pressure initial condition |
Lagrangian solver extensions (lagrangian/)¶
Hierarchy:
| Class | Purpose |
|---|---|
absractStochasticLagrangianSolver_toolkitExtension (abstractLagrangianSolver.py) |
Base for Lagrangian solvers — dispersion flow fields, source generation, case results; holds an analysis object (concentration methods) |
StochasticLagrangianSolver_toolkitExtension (StochasticLagrangianSolver.py) |
Concrete implementation; overrides createDispersionFlowField (default dispersion fields such as ustar, distanceFromWalls) |
OFLSMToolkit (LSM/toolkit.py) |
Separate OpenFOAM + LSM coupling toolkit — particle data loading, concentration calculation |
Dispersion flow field pipeline (createDispersionFlowField)¶
The most complex operation in the Lagrangian toolkit — creates a dispersion case directory from an existing flow simulation. The method is decomposed into a readable orchestrator calling private helpers:
| Step | Helper | Purpose |
|---|---|---|
| 1 | _locateOriginalFlowCase() |
Find flow in DB or filesystem |
| 2 | _detectParallelAndTimesteps() |
Detect parallel case + discover time directories |
| 3 | _buildTimeMapping() |
Build (original→dispersion) time mapping for steady-state or dynamic flows |
| 4 | _checkAndRegisterDispersionInDB() |
Query DB for existing record, handle overwrite |
| 5 | _prepareDispersionDirectory() |
Copy/link constant/system/0 + per-timestep data + write dispersion fields |
| 6 | _finalizeDispersionInDB() |
Register or update DB document |
Time mapping logic:
- Steady-state: freezes the flow at one timestep — maps to
t=0andt=dispersionDuration - Dynamic: shifts each flow timestep so dispersion starts at
t=0. If the flow ends before the dispersion duration, the last timestep is repeated.
Case results and caching¶
getCaseResults and getCaseConcentrationsEulerian share the same cache/load/compute pattern via 7 shared helpers:
| Helper | Purpose |
|---|---|
_resolveCaseDescriptorName() |
Extract name from str or MetadataFrame |
_checkCache() |
Lookup cache, handle overwrite, clean stale entries |
_locateCaseDirectory() |
DB or filesystem case path resolution |
_discoverTimesteps() |
Find numeric time dirs, filter system dirs |
_loadCaseDataViaDask() |
Parallel/serial detection + Dask distributed map |
_saveToCacheParquet() |
Parquet save + DB registration (Lagrangian) |
_saveToCacheNetCDF() |
xarray groupby+sum + netCDF save (Eulerian concentrations) |
Dispersion case directory (createDispersionCaseDirectory)¶
Validates DB consistency, handles name/parameter conflicts, and creates the case directory:
| Helper | Purpose |
|---|---|
_resolveDispersionFlowField() |
DB lookup for flow field, name canonicalization |
_checkDBConsistency() |
Detect name conflicts + parameter duplicates |
_resolveDirectoryConflict() |
Handle existing directory with rewrite flag |
_syncDispersionToDB() |
Add new or update existing DB record |
Concentration field computation (calcConcentrationFieldFullMesh)¶
Bins Lagrangian particles onto a Cartesian mesh, stores as partitioned netCDF:
| Helper | Purpose |
|---|---|
_removeConcentrationCache() |
Clean old cache files + DB record |
_createConcentrationCacheDoc() |
Counter-based path + DB registration |
_processConcentrationPartition() |
Per-Dask-partition timestep binning |
_padRemainingTimesteps() |
Zero-fill to full dispersion duration |
_writeConcentrationPartition() |
Concat + rename axes + write netCDF |
Source generation¶
| Method | Description |
|---|---|
makeSource_Point/Circle/Sphere/Cylinder/Rectangle/Cube(x, y, z, nParticles, ...) |
Generate particle source geometries (sourcesTypeList lists them) |
writeParticlePositionFile(x, y, z, nParticles, dispersionName, type="Point", ...) |
Write particle initial positions |
OFLSMToolkit (lagrangian/LSM/toolkit.py)¶
Inherits from abstractToolkit (not from OFToolkit) and provides OpenFOAM + LSM coupling. Registry name OF_LSM (no toolkitHome constant); constructor OFLSMToolkit(projectName, casePath=None, cloudName="kinematicCloud", filesDirectory=None, parallelCase=False, connectionName=None).
Known issue in master
OFLSMToolkit.__init__ calls toolkitHome.getToolkit(toolkitName=toolkitHome.GIS_TOPOGRAPHY, ...) (lagrangian/LSM/toolkit.py:97), but ToolkitHome defines no GIS_TOPOGRAPHY constant (only GIS_RASTER_TOPOGRAPHY / GIS_VECTOR_TOPOGRAPHY), so instantiation raises AttributeError.
| Property | Description |
|---|---|
casePath |
OpenFOAM case directory |
cloudName |
Lagrangian particle cloud name |
parallelCase |
Decomposed (parallel) case flag |
sourcesFactory |
sourcesFactoryTool for source geometries |
topography |
GIS topography toolkit reference |
| Method | Description |
|---|---|
loadData(times=None, saveMode=TOOLKIT_SAVEMODE_NOSAVE, withVelocity=False, withReleaseTimes=False, withMass=False, releaseTime=0, cloudName="kinematicCloud", casePath=None, **kwargs) |
Extract particle positions/velocities/masses as dask DataFrame |
makeSource(x, y, z, nParticles, type="Point", fileName="kinematicCloudPositions", **kwargs) |
Write a particle source file via sourcesFactory |
makeDistanceFromGround(times, ground="ground", fileName="cellHeights", resolution=10, ...) |
Compute ground distance field |
makeUstar(times, fileName="ustar", ground="ground", heightLimits=[1, 2], ...) |
Compute friction velocity field (decomposed into 6 helpers) |
createRootCaseMeshLink(rootCase) |
Link the root case mesh into the case |
to_paraview_CSV(data, outputdirectory, filename, timeFactor=1) |
Export particles as timestep CSV |
toUnstructuredVTK(data, outputdirectory, filename, timeNameOutput=True) |
Export as VTU particle files |
toStructuredVTK(data, outputdirectory, filename, extents, dxdydz, timeNameOutput=True) |
Export as structured VTK grid |
makeUstar helper decomposition:
| Helper | Purpose |
|---|---|
_loadOrComputeCellData() |
Cached cell height data lookup/computation |
_parseOFFieldHeader() |
Find internalField start + cell count |
_parseBoundaryStart() |
Find boundaryField line number |
_extractVelocityField() |
Parse OF vector, compute magnitude |
_interpolateNearGroundValues() |
Regularize to 2D grid, interpolate with (x,y) cache |
_writeScalarField() |
Assemble header + values + boundary → write file |
Analysis class on OFLSMToolkit (lagrangian/LSM/toolkit.py; the Lagrangian solver extension's analysis class in abstractLagrangianSolver.py has methods of the same names, defaulting to x/y/z instead of globalX/globalY/globalZ):
| Method | Description |
|---|---|
calcConcentrationPointWise(data, dxdydz, xfield="globalX", ...) |
Cell-binned concentration from particle data |
calcDocumentConcentrationPointWise(dataDocument, dxdydz, ...) |
Same with DB caching |
calcConcentrationTimeStepFullMesh(timeData, extents, dxdydz, ...) |
Full domain regular grid concentration |
calcConcentrationFieldFullMesh(dataDocument, extents, dxdydz, ...) |
Full pipeline: load particles → bin → write netCDF (the helper decomposition above applies to the solver-extension version, not this one) |
getConcentrationField(dataDocument, returnFirst=True, **metadata) |
Load a cached concentration field |
getMassFromLog(logFile, solver="StochasticLagrangianSolver") |
Parse released/escaped mass from a solver log |
Preprocessing objects (preprocessOFObjects/)¶
The preprocessing system provides an abstraction for OpenFOAM field files:
| Class | Purpose |
|---|---|
OFObject |
Abstract base — field name, dimensions, regions (internalField/boundaryField), write to disk |
OFField(OFObject) |
Concrete field — initialize empty, set uniform values, add boundaries, read from case, convert to DataFrame |
OFObjectHome |
Field catalog — predefined fields (U, p, k, epsilon, T, ...) with correct dimensions for incompressible/compressible flows |
OFObjectHome predefined fields: U, p, p_rgh, epsilon, omega, alphat, nut, k, T, Tbackground, cellCenters, Hmix, ustar, distanceFromWalls
Key OFObjectHome methods:
| Method | Description |
|---|---|
getEmptyField(fieldName, flowType, noOfProc=None, addParallelProc=False) |
Create empty field with correct dimensions |
getEmptyFieldFromCase(fieldName, flowType, caseDirectory, internalValue=None, readParallel=True) |
Create field pre-loaded with boundaries from an existing case |
readFieldFromCase(fieldName, flowType, caseDirectory, timeStep=0, readParallel=True) |
Read complete field data |
readFieldAsDataFrame(fieldName, caseDirectory, times=0, readParallel=True, filterInternalPatches=False) |
Read field across multiple time steps as DataFrame |
addFieldDefinitions(fieldName, dimensions, fieldType, fieldComputation=FIELDCOMPUTATION_EULERIAN, compressible_dimensions=None, overwrite=False) |
Register custom field definitions |
VTK post-processing pipeline (postProcess/)¶
The VTK pipeline provides a ParaView-integrated post-processing system with DB caching.
Pipeline architecture:
| Class | Purpose |
|---|---|
VTKPipeLine |
Pipeline container — creates/manages filters (addFilter(name, filterType, write=True, params=None)), exports to JSON; obtained via OFToolkit.analysis.getVTKPipeline() |
registeredVTKPipeLine |
Pipeline bound to a specific case — executes filters, caches results |
VTKFilter |
Base filter node — tree structure with downstream children |
paraviewOpenFOAM (pvOpenFOAMBase.py) |
ParaView backend — reads OpenFOAM cases, writes results |
Available filters:
| Filter class | Type constant | Purpose |
|---|---|---|
vtkFilter_Slice |
FILTER_SLICE |
Planar slice through the domain |
vtkFilter_PlotOverLine |
FILTER_PLOTOVERLINE |
Sample along a line |
vtkFilter_CellCenters |
FILTER_CELLCENTERS |
Extract cell center values |
vtkFilter_ExtractBlock |
FILTER_EXTRACTBLOCK |
Extract specific mesh regions/patches |
vtkFilter_DescriptiveStatistics |
— | Compute summary statistics |
vtkFilter_IntegrateVariables |
FILTER_INTEGRATEVARIABLES |
Integrate fields over domain |
Data flow:
VTKPipeLine.registerPipeline(nameOrWorkflowFileOrJSONOrResource, serverName=None, caseType=CASETYPE_DECOMPOSED)
→ registeredVTKPipeLine
→ getData(regularMesh, filterName=None, timeList=None, latestTime=False, fieldNames=None, overwrite=False)
(or getRegularData(...) / getNonRegularData(...))
→ checks DB cache
→ if miss: runs ParaView pipeline
→ saves to parquet (non-regular) or ZARR (regular mesh)
→ stores metadata document in DB
→ returns dict of filter results
Filter tree: Filters form a tree — each filter can have downstream children. The pipeline traverses the tree depth-first, executing each filter and optionally writing results to disk.
getData decomposition (was 181 lines, CC 15):
| Step | Helper | Purpose |
|---|---|---|
| 1 | _resolveRequestedFilters() |
Coerce None/str/list → concrete filter name list |
| 2 | _parseTimeList() |
Handle None (all times), "start:end" range, or explicit list |
| 3 | _filterCachedTimesteps() |
Subtract already-cached timesteps for incremental computation |
| 4 | _buildAndExecuteParaViewPipeline() |
Create reader → build filter tree → execute → write results |
| 5 | _updateCacheDB() |
Merge new timesteps into existing DB records |
writeCase decomposition (paraviewOpenFOAM.writeCase in pvOpenFOAMBase.py; was 127 lines, CC 20):
| Step | Helper | Purpose |
|---|---|---|
| 1 | _removeOldOutputs() |
Clean existing files before overwrite |
| 2 | _ensureOutputDirs() |
Create output directories |
| 3 | _resolveTimeList() |
Determine timesteps from reader or caller |
| 4 | _writeTimeStepBlocks() |
Stream timesteps in fixed-size blocks to temp files |
| 5 | _collectTmpFiles() |
Glob temp block files per filter |
| 6 | _mergeToFinalOutput() |
Dispatch to _mergeZarr or _mergeParquet |
| 7 | _atomicReplace() |
.final staging → rename |
| 8 | _cleanupTmpFiles() |
Remove temp blocks after merge |
The merge step supports both regular meshes (xarray → zarr with time-concatenation) and unstructured meshes (dask → parquet with repartitioning to ~100 MB chunks). The atomic .final → rename pattern ensures no partial writes are visible.
Template system¶
There is no saveTemplate / loadTemplate API in master. Templates are Hermes flow/node templates stored as toolkit datasources (desc.component = "Flow" or "Node", flows also tagged with solver) and read through hermesWorkflowToolkit:
listHermesSolverTemplates(solverName)/getHermesFlowTemplate(hermesFlowName)— list and fetch flow templateslistHermesNodesTemplates()/getHermesNodeTemplate(hermesNodeName)— list and fetch node templates- CLI:
hera-openFoam <solver> templates listandhera-openFoam <solver> templates get
OFToolkit.template_add(name, objFile, workflowObj=None) exists but is an unimplemented stub (pass, toolkit.py:502-525).
CLI¶
The hera-openFoam CLI (hera/bin/hera-openFoam, handlers in openFoam/CLI.py) exposes per-solver subcommands (simpleFoam, pimpleFoam, oguraPhillipShallowConvectionFOAM, buoyantReactingFoam, scalarTransportFoam, homogenousWindLogProfile, stochasticLagrangian) with actions such as templates, executeWorkflow, list, delete, blockMesh, IC, case, postProcess. See the CLI reference.