Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion OpenFOAM/OutWit.Controller.OpenFOAM.Model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
[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

/// <summary>The lines of a section: from its heading to the next heading of any level.</summary>
private List<string> 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();
}

/// <summary>The cells of a section's table rows, header and separator left out.</summary>
private List<string[]> 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<string> 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<FoamResponseKind>()));
}

[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
}
5 changes: 4 additions & 1 deletion OpenFOAM/OutWit.Controller.OpenFOAM/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
192 changes: 192 additions & 0 deletions OpenFOAM/SUPPORTED-INPUTS.md
Original file line number Diff line number Diff line change
@@ -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.
Loading