From 68dcf108a233b4b33d62f1a556eea209ffc1684a Mon Sep 17 00:00:00 2001 From: Dmitry Ratner <6830384+dmitrat@users.noreply.github.com> Date: Tue, 29 Sep 2026 06:25:53 +0300 Subject: [PATCH] docs(openfoam): SUPPORTED-INPUTS.md - what runs and what is refused, held to the rules The build and its platforms, the case and the paths and constructs it may not carry, the recipe with the allow-list, the built-in step and the forbidden flags, the responses, the solver classes, what comes back, the two study forms, the trademark statement and the GPL note. Guard tests hold the document's tables to FoamAllowList, FoamRecipeRules, FoamResponseKind, FoamSolverClasses and the controller's limits, and the document and both READMEs to the trademark statement word for word; the READMEs link to it. Co-Authored-By: Claude Opus 5.5 --- .../README.md | 3 +- .../Rules/FoamSupportedInputsDocumentTests.cs | 135 ++++++++++++ OpenFOAM/OutWit.Controller.OpenFOAM/README.md | 5 +- OpenFOAM/SUPPORTED-INPUTS.md | 192 ++++++++++++++++++ 4 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamSupportedInputsDocumentTests.cs create mode 100644 OpenFOAM/SUPPORTED-INPUTS.md diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/README.md b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/README.md index 7910db2..7f2c221 100644 --- a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/README.md +++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/README.md @@ -18,7 +18,8 @@ rules both enforce: the response set evaluated on the node right after the run. - `FoamArtifactPolicyData` — what of the finished case travels back. -`Rules/` holds what a case may be, as code every party runs: `FoamAllowList` +`Rules/` holds what a case may be, as code every party runs (published as +[SUPPORTED-INPUTS.md](https://github.com/OmnibusCloud/Controllers/blob/main/OpenFOAM/SUPPORTED-INPUTS.md)): `FoamAllowList` (the utilities, the solver shape, and the controller's own step `restore0Dir -processor`, which puts the initial fields into the processor directories of a case meshed on its decomposed form), `FoamRecipeRules` diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamSupportedInputsDocumentTests.cs b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamSupportedInputsDocumentTests.cs new file mode 100644 index 0000000..d21c5ba --- /dev/null +++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamSupportedInputsDocumentTests.cs @@ -0,0 +1,135 @@ +using System.Text.RegularExpressions; +using OutWit.Controller.OpenFOAM.Model; +using OutWit.Controller.OpenFOAM.Model.Rules; +using OutWit.Controller.OpenFOAM.Runtime; +using OutWit.Controller.OpenFOAM.Tests.Utils; + +namespace OutWit.Controller.OpenFOAM.Tests.Model.Rules; + +/// +/// The supported-inputs document is held to the rules it publishes: the +/// allow-list and its parallel column, the built-in steps, the forbidden +/// flags, the step limit, the response kinds, the solver classes and the +/// limits the controller applies; and the document and both READMEs carry +/// the trademark statement word for word. +/// +[TestFixture] +public class FoamSupportedInputsDocumentTests +{ + private const string STATEMENT = + "OpenFOAM® is a registered trademark of OpenCFD Limited. This offering is not approved or endorsed by OpenCFD Limited, " + + "producer and distributor of the OpenFOAM software via www.openfoam.com, and owner of the OPENFOAM® and OpenCFD® trade marks."; + + private static readonly Regex CODE = new("`([^`]+)`", RegexOptions.Compiled); + + private string m_openFoamRoot = null!; + + private string m_document = null!; + + [OneTimeSetUp] + public void Setup() + { + var solutionRoot = OpenFOAMTestPaths.FindSolutionRoot(); + if (solutionRoot == null) + Assert.Ignore("Solution root not found"); + + m_openFoamRoot = Path.Combine(solutionRoot, "OpenFOAM"); + m_document = File.ReadAllText(Path.Combine(m_openFoamRoot, "SUPPORTED-INPUTS.md")).Replace("\r\n", "\n"); + } + + #region Tools + + /// The lines of a section: from its heading to the next heading of any level. + private List Section(string heading) + { + var lines = m_document.Split('\n'); + var start = Array.IndexOf(lines, heading); + Assert.That(start, Is.GreaterThanOrEqualTo(0), $"the document has no '{heading}'"); + + return lines.Skip(start + 1).TakeWhile(line => !line.StartsWith('#')).ToList(); + } + + /// The cells of a section's table rows, header and separator left out. + private List Rows(string heading) + { + return Section(heading) + .Where(line => line.StartsWith('|')) + .Skip(2) + .Select(line => line.Trim('|').Split('|').Select(cell => cell.Trim()).ToArray()) + .ToList(); + } + + private static List CodeIn(string text) + { + return CODE.Matches(text).Select(match => match.Groups[1].Value).ToList(); + } + + private static string Flat(string text) + { + return Regex.Replace(text, @"\s+", " "); + } + + #endregion + + #region Rule Tests + + [Test] + public void TheUtilitiesTableIsTheAllowListTest() + { + var rows = Rows("### Utilities"); + + Assert.That(rows.Select(row => CodeIn(row[0]).Single()), Is.EqualTo(FoamAllowList.UTILITIES)); + Assert.That(rows.Where(row => row[1] == "yes").Select(row => CodeIn(row[0]).Single()), Is.EquivalentTo(FoamAllowList.PARALLEL_CAPABLE_UTILITIES)); + } + + [Test] + public void TheBuiltInStepsAndTheForbiddenFlagsAreTheRulesTest() + { + var builtIn = Section("### Built-in steps").Where(line => line.StartsWith("- ")).Select(line => CodeIn(line)[0].Split(' ')[0]); + Assert.That(builtIn, Is.EqualTo(FoamAllowList.BUILT_IN_STEPS)); + Assert.That(m_document, Does.Contain($"`{FoamAllowList.RESTORE_INITIAL_FIELDS} {FoamAllowList.PROCESSOR_FORM}`")); + + var arguments = Section("### Arguments"); + var flags = arguments[arguments.FindIndex(line => line.EndsWith("refused in a recipe:")) + 2]; + Assert.That(CodeIn(flags), Is.EqualTo(FoamAllowList.FORBIDDEN_FLAGS)); + } + + [Test] + public void TheLimitsAreTheControllersTest() + { + Assert.That(Flat(m_document), Does.Contain($"at most **{FoamRecipeRules.MAX_STEPS}**")); + Assert.That(Flat(m_document), Does.Contain($"at most {FoamDecomposition.MAX_DEFAULT_RANKS} ranks")); + Assert.That(Flat(m_document), Does.Contain($"larger than {FoamCaseContentRules.MAX_SCANNED_BYTES / (1024 * 1024)} MB")); + } + + [Test] + public void TheResponseKindsAreTheVocabularyTest() + { + Assert.That(Rows("## Responses").Select(row => CodeIn(row[0]).Single()), Is.EqualTo(Enum.GetNames())); + } + + [Test] + public void TheSolverClassesAreTheClassMapTest() + { + var published = Rows("## Solver classes") + .SelectMany(row => CodeIn(row[1]).Select(application => (Application: application, Class: CodeIn(row[0]).Single()))) + .ToDictionary(pair => pair.Application, pair => pair.Class); + + Assert.That(Rows("## Solver classes").Select(row => CodeIn(row[0]).Single()), Is.EqualTo(FoamSolverClasses.ALL)); + Assert.That(published, Is.EquivalentTo(FoamSolverClasses.APPLICATIONS)); + } + + #endregion + + #region Notice Tests + + [TestCase("SUPPORTED-INPUTS.md")] + [TestCase("OutWit.Controller.OpenFOAM/README.md")] + [TestCase("OutWit.Controller.OpenFOAM.Model/README.md")] + public void EveryPageCarriesTheTrademarkStatementTest(string page) + { + Assert.That(Flat(File.ReadAllText(Path.Combine(m_openFoamRoot, page))), Does.Contain(STATEMENT)); + } + + #endregion +} diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM/README.md b/OpenFOAM/OutWit.Controller.OpenFOAM/README.md index af7a4ab..945e3d5 100644 --- a/OpenFOAM/OutWit.Controller.OpenFOAM/README.md +++ b/OpenFOAM/OutWit.Controller.OpenFOAM/README.md @@ -28,7 +28,10 @@ the response request, the token coverage - live in `OutWit.Controller.OpenFOAM.Model` (`Rules/`), so the node, the Sweep host and an initiator's preflight refuse the same things with the same sentences. What needs the kit or the materialised files (executables, libraries, -run-time code) is checked here, on the node. +run-time code) is checked here, on the node. The whole of it - the build and +its platforms, the allow-list, what a case may not carry, the responses, the +solver classes - is published, held to the rules by the tests, in +[SUPPORTED-INPUTS.md](https://github.com/OmnibusCloud/Controllers/blob/main/OpenFOAM/SUPPORTED-INPUTS.md). ## What a case may contain diff --git a/OpenFOAM/SUPPORTED-INPUTS.md b/OpenFOAM/SUPPORTED-INPUTS.md new file mode 100644 index 0000000..7ec9a04 --- /dev/null +++ b/OpenFOAM/SUPPORTED-INPUTS.md @@ -0,0 +1,192 @@ +# OpenFOAM: supported inputs + +What the OpenFOAM controller runs and what it refuses, as the rules in +`OutWit.Controller.OpenFOAM.Model` (`Rules/`) state them. The node, the Sweep +host and an initiator's preflight apply the same rules and refuse with the same +sentences; the tables below are held to those constants by the controller's +tests, so a rule that changes cannot leave this page behind. + +## The build + +Every case runs on **OpenFOAM v2606**, whole, on one node, from the pinned kit +of [OmnibusCloud/OpenFOAM](https://github.com/OmnibusCloud/OpenFOAM) (release +`openfoam-v2606-3`). There is no conversion and no other build: a case written +for another OpenFOAM line or version runs only if v2606 accepts it. + +| Platform | Kit | Notes | +|---|---|---| +| Linux x64 | `linux-x64` | built on Ubuntu 22.04: glibc 2.35 or newer | +| macOS arm64 | `osx-arm64` | Apple Silicon | +| Windows x64 | `win-x64` | a parallel step runs under MS-MPI when the node has it installed, serially otherwise | + +A node takes a case when it has at least 4 GB of RAM and 4 GB of free temporary +storage. A parallel step runs on the ranks the task asks for; "all cores" means +at most 16 ranks. The controller writes the `decomposeParDict` for that count +(scotch); a `decomposeParDict` the case carries is not used. + +## The case + +A case is a **reconstructed** case directory: `system/controlDict` with an +`application` entry, the time directories at the root (WitSweep uploads a case's +`0.orig/` as `0/` when it has no `0/`), and either a mesh in `constant/polyMesh` +or a step that makes one. Every file travels as it is: the node substitutes a +study's tokens byte for byte, adds the function objects the responses need and, +for a parallel run, writes its own `decomposeParDict`; nothing else changes. + +A case file path is refused when it + +- uses backslashes, contains whitespace, has empty or `.` segments, or leaves + the case directory; +- is a log of an earlier run (`log.*` at the root); +- lies under `postProcessing/` of an earlier run (its values would pass for this + run's); +- lies under `processor*/` - a decomposed case is not accepted, submit the + reconstructed case; +- repeats another path, or differs from one by letter case alone (one file on a + Windows or macOS node). + +A case is refused, by file and line, when it carries code that would be compiled +while it runs - the kit ships no compiler, and a pool never runs code it cannot +name: + +| Construct | Refused because | +|---|---| +| `codeStream`, `#codeStream` | compiles C++ at run time | +| `#calc` | compiles C++ at run time (`#eval` is the in-built evaluator and is allowed) | +| a `coded` boundary condition or function object | compiles C++ at run time | +| `dynamicCode/` | compiled run-time code the case carries | +| `libs (...)` naming a library the kit does not have | the library is not there | +| `#include`, `#includeIfPresent`, `#includeFunc`, `#includeEtc` reaching outside the case | the run must be reproducible from the case alone | +| a UTF-8 byte order mark at the start of a file | OpenFOAM reads it as part of the first word | + +Files larger than 8 MB (meshes, large fields) are not read for these +constructs. + +## The recipe + +A recipe is an ordered list of steps, at most **32**, one of which runs the +case's application - the solver its `controlDict` names. A step is a utility of +the allow-list, a solver, or a step the controller does itself. There is no +shell step and no custom command. + +**Solvers** are the kit's executables whose name ends in `Foam` (`simpleFoam`, +`interFoam`, `rhoPimpleFoam`, ...) other than the utilities below +(`potentialFoam` is a utility); every solver may run in parallel. + +### Utilities + +| Utility | Parallel | +|---|---| +| `blockMesh` | | +| `surfaceFeatureExtract` | | +| `snappyHexMesh` | yes | +| `extrudeMesh` | | +| `refineMesh` | yes | +| `setFields` | yes | +| `mapFields` | | +| `topoSet` | yes | +| `createPatch` | yes | +| `createBaffles` | yes | +| `transformPoints` | | +| `renumberMesh` | yes | +| `checkMesh` | yes | +| `decomposePar` | | +| `reconstructPar` | | +| `reconstructParMesh` | | +| `postProcess` | yes | +| `foamDictionary` | | +| `potentialFoam` | yes | +| `setsToZones` | | +| `subsetMesh` | | +| `splitMeshRegions` | yes | +| `mergeMeshes` | | +| `mergeOrSplitBaffles` | | +| `extrudeToRegionMesh` | | +| `refineHexMesh` | | +| `collapseEdges` | | +| `extrude2DMesh` | | +| `setAlphaField` | | +| `makeFaMesh` | yes | + +A parallel step naming a utility not marked here is refused. + +### Built-in steps + +- `restore0Dir -processor` - puts the initial fields into every processor + directory after `decomposePar` (what a case meshed in parallel needs). Its only + form; it needs a `decomposePar` step before it and never runs under MPI. + +### Arguments + +A step's arguments are flags (`-overwrite`, `-latestTime`, ...) and plain values. +No argument may point outside the case, and `-parallel` is never an argument - +it is the step's parallel switch. These flags are decided by the controller and +refused in a recipe: + +`-case`, `-decomposeParDict`, `-fileHandler`, `-hostRoots`, `-roots`, `-lib`, `-libs` + +## Responses + +A response is a number the node reads from what OpenFOAM writes, by a function +object the case already runs or one the controller adds for the run: + +| Kind | Function object | Needs | +|---|---|---| +| `ForceCoeffs` | `forceCoeffs` | at least one patch; the reference values (`magUInf`, `lRef`, `Aref`, ...) as parameters | +| `Forces` | `forces` | at least one patch | +| `PatchValue` | `surfaceFieldValue` | exactly one patch, at least one field, an operation (`areaAverage`, `areaIntegrate`, `min`, `max`, ...) | +| `VolumeValue` | `volFieldValue` | at least one field, an operation (`volAverage`, `volIntegrate`, `min`, `max`, ...) | +| `FieldMinMax` | `fieldMinMax` | at least one field | +| `Probe` | `probes` | at least one field, a `probeLocations` parameter | + +A response's name is a word, unique in the request, and not the name of a file +the case carries in `system/`. A value is reported only from the run's final +time: a function object that stopped writing earlier is left out and named in +`log.responses`. In a parameter study a response parameter may carry the +study's token (a reference speed that follows the swept speed). + +## Solver classes + +The class of a solver prices a run for the scheduler and, in WitSweep, estimates +its memory from measured curves. A solver outside the table has no class: it +runs, priced as the unknown class. + +| Class | Solvers | +|---|---| +| `incompressible-steady` | `simpleFoam`, `porousSimpleFoam`, `SRFSimpleFoam`, `overSimpleFoam`, `boundaryFoam`, `adjointOptimisationFoam`, `adjointShapeOptimizationFoam` | +| `incompressible-transient` | `pimpleFoam`, `pisoFoam`, `icoFoam`, `nonNewtonianIcoFoam`, `SRFPimpleFoam`, `overPimpleDyMFoam`, `shallowWaterFoam` | +| `compressible-steady` | `rhoSimpleFoam`, `rhoPorousSimpleFoam`, `overRhoSimpleFoam` | +| `compressible-transient` | `rhoPimpleFoam`, `rhoCentralFoam`, `rhoPimpleAdiabaticFoam`, `overRhoPimpleDyMFoam`, `sonicFoam`, `sonicDyMFoam`, `sonicLiquidFoam` | +| `thermal-steady` | `buoyantSimpleFoam`, `buoyantBoussinesqSimpleFoam`, `chtMultiRegionSimpleFoam` | +| `thermal-transient` | `buoyantPimpleFoam`, `buoyantBoussinesqPimpleFoam`, `overBuoyantPimpleDyMFoam`, `chtMultiRegionFoam`, `laplacianFoam`, `solidFoam` | +| `multiphase-transient` | `interFoam`, `interIsoFoam`, `interMixingFoam`, `overInterDyMFoam`, `interPhaseChangeFoam`, `interPhaseChangeDyMFoam`, `compressibleInterFoam`, `compressibleInterIsoFoam`, `compressibleInterDyMFoam`, `multiphaseInterFoam`, `multiphaseEulerFoam`, `twoPhaseEulerFoam`, `reactingTwoPhaseEulerFoam`, `reactingMultiphaseEulerFoam`, `driftFluxFoam`, `cavitatingFoam`, `twoLiquidMixingFoam`, `potentialFreeSurfaceFoam` | + +## What comes back + +Every run returns its facts - converged or not, iterations, final residuals, +cells, `checkMesh`'s verdict, the time of each step, exit code, warnings - and +its responses. What else comes back is the task's artifact policy: nothing, the +final time and the mesh, or every written time; the step logs; `postProcessing/`. +A node keeps nothing: what is not returned is gone when the run ends. + +## Studies + +`OutWit.Controller.Sweep` runs a parameter study over a case in one of two +forms: + +- **A case with tokens.** Values in the case's dictionaries are replaced by + tokens (`{{oc1}}`, ...) in the files marked as templated; every variant is the + same case with its own values, substituted on the node byte for byte. Before + any node sees a task, the Sweep host checks that every token has a place in a + templated file and every place a declared token. +- **A case set.** Every variant is its own ready case - its own tree and recipe, + nothing templated - and the study carries what the cases share: the ranks, the + responses, the artifact policy. + +--- + +OpenFOAM® is a registered trademark of OpenCFD Limited. This offering is not +approved or endorsed by OpenCFD Limited, producer and distributor of the +OpenFOAM software via www.openfoam.com, and owner of the OPENFOAM® and +OpenCFD® trade marks. The kit is built from OpenFOAM's sources under the +GPL-3.0; the kit's repository carries the licence and the source offer.