diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/OutWit.Controller.OpenFOAM.Model.csproj b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/OutWit.Controller.OpenFOAM.Model.csproj
index 0839d11..d07b89f 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/OutWit.Controller.OpenFOAM.Model.csproj
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/OutWit.Controller.OpenFOAM.Model.csproj
@@ -5,7 +5,7 @@
enable
OpenFOAM
- 1.2.0
+ 1.3.0
Shared data types of the OpenFOAM case controller and the rules a case obeys: the case (base files, allow-listed recipe, response request, artifact policy), one variant's run (the case and its token values) and its outcome (per-step exit codes and times, convergence facts, the extracted response row, the artifact reference); the allow-list, recipe grammar, case-path, response, token-coverage and run-time-code rules, byte-exact substitution and the solver-class vocabulary, shared by the node, the Sweep host and client preflight. Consumed by the node controller, the Sweep orchestration controller and client applications.
openfoam;cfd;case;sweep;parameter-study;witengine;witcloud;controller;data-types
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamResponseRules.cs b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamResponseRules.cs
index f847425..9dbd4e1 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamResponseRules.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamResponseRules.cs
@@ -19,6 +19,13 @@ public static class FoamResponseRules
private const string SYSTEM_PREFIX = "system/";
+ ///
+ /// What a placeholder counts as when a value is checked before the
+ /// variant supplies it: a number. The node checks the value again once it
+ /// is substituted.
+ ///
+ private const string TOKEN_STAND_IN = "0";
+
private static readonly Regex WORD = new("^[A-Za-z][A-Za-z0-9_]*$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex OPERATION = new("^[A-Za-z][A-Za-z0-9]*$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
@@ -75,7 +82,7 @@ public static IReadOnlyList Validate(FoamExtractionRequestData? request,
{
if (!WORD.IsMatch(parameter.Name))
findings.Add($"{prefix}: parameter '{parameter.Name}' is not a keyword.");
- else if (!VALUE.IsMatch(parameter.Value))
+ else if (!VALUE.IsMatch(FoamTemplating.TOKEN.Replace(parameter.Value, TOKEN_STAND_IN)))
findings.Add($"{prefix}: the value of '{parameter.Name}' is not a plain dictionary value.");
}
}
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamTemplating.cs b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamTemplating.cs
index 2d9bc76..40ec1bf 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamTemplating.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Model/Rules/FoamTemplating.cs
@@ -145,6 +145,29 @@ public static IReadOnlyList CheckCoverage(IReadOnlyList tokens,
return findings;
}
+ ///
+ /// Checks the placeholders of a study's responses: a response parameter
+ /// may follow a swept value (magUInf {{oc1}} beside the swept
+ /// speed), but only a token the study declares - otherwise every variant
+ /// would be refused on its node.
+ ///
+ /// The study's declared tokens.
+ /// The responses; null asks for none.
+ /// Findings, one sentence each; empty when every response token is declared.
+ public static IReadOnlyList CheckResponseCoverage(IReadOnlyList tokens, FoamExtractionRequestData? request)
+ {
+ var declared = new HashSet(tokens, StringComparer.Ordinal);
+ var findings = new List();
+
+ foreach (var response in request?.Responses ?? [])
+ {
+ foreach (var token in response.Parameters.SelectMany(parameter => LeftoverTokens(parameter.Value)).Distinct(StringComparer.Ordinal).Where(token => !declared.Contains(token)))
+ findings.Add($"Response '{response.Name}': token {token} is not declared by the study.");
+ }
+
+ return findings;
+ }
+
private static Dictionary ValuesOf(IReadOnlyList substitutions, Func view)
{
var values = new Dictionary(StringComparer.Ordinal);
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Extraction/FoamFunctionObjectWriterTests.cs b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Extraction/FoamFunctionObjectWriterTests.cs
index 0b1cc74..9bbb3cb 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Extraction/FoamFunctionObjectWriterTests.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Extraction/FoamFunctionObjectWriterTests.cs
@@ -133,6 +133,44 @@ public void AResponseNamedLikeAFileTheCaseShipsIsRefusedAndNothingIsWrittenTest(
Assert.That(File.Exists(Path.Combine(m_case, "system", "inletP")), Is.False, "the other responses are not written either");
}
+ [Test]
+ public void AResponseParameterTakesTheVariantsValueTest()
+ {
+ var request = MotorBikeRequest();
+ request.Responses[0].Parameters.Single(parameter => parameter.Name == "magUInf").Value = "{{oc1}}";
+
+ var findings = FoamFunctionObjectWriter.Write(m_case, request, [new FoamTokenValueData { Token = "{{oc1}}", Value = "25" }]);
+
+ Assert.That(findings, Is.Empty);
+ var coefficients = File.ReadAllText(Path.Combine(m_case, "system", "coeffs"));
+ Assert.That(coefficients, Does.Contain("magUInf 25;"));
+ Assert.That(coefficients, Does.Not.Contain("{{"));
+ }
+
+ [Test]
+ public void AValueThatIsNotPlainAfterSubstitutionIsRefusedTest()
+ {
+ var request = MotorBikeRequest();
+ request.Responses[0].Parameters.Single(parameter => parameter.Name == "magUInf").Value = "{{oc1}}";
+
+ var findings = FoamFunctionObjectWriter.Write(m_case, request, [new FoamTokenValueData { Token = "{{oc1}}", Value = "25; #calc \"1\"" }]);
+
+ Assert.That(findings, Is.EqualTo(new[] { "Response 'coeffs': the value of 'magUInf' is not a plain dictionary value." }));
+ Assert.That(File.Exists(Path.Combine(m_case, "system", "coeffs")), Is.False);
+ }
+
+ [Test]
+ public void ATokenWithoutAValueInTheVariantIsNamedTest()
+ {
+ var request = MotorBikeRequest();
+ request.Responses[0].Parameters.Single(parameter => parameter.Name == "magUInf").Value = "{{oc2}}";
+
+ var findings = FoamFunctionObjectWriter.Write(m_case, request, [new FoamTokenValueData { Token = "{{oc1}}", Value = "25" }]);
+
+ Assert.That(findings, Is.EqualTo(new[] { "Response 'coeffs': token {{oc2}} has no value in this variant." }));
+ Assert.That(File.Exists(Path.Combine(m_case, "system", "coeffs")), Is.False);
+ }
+
[Test]
public void ANullOrEmptyRequestWritesNothingTest()
{
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamResponseRulesTests.cs b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamResponseRulesTests.cs
index 71f233b..bb8c9d0 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamResponseRulesTests.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamResponseRulesTests.cs
@@ -105,5 +105,26 @@ public void AQuotedSelectorIsPlainRegularExpressionTextTest()
Assert.That(FoamResponseRules.IsQuotedRegex("\"a;b\""), Is.False, "a semicolon ends a dictionary entry");
}
+ [Test]
+ public void APlaceholderStandsForAValueTheSweepSuppliesTest()
+ {
+ static FoamExtractionRequestData Coefficients(string value) => new()
+ {
+ Responses =
+ [
+ new FoamResponseSpecData
+ {
+ Name = "coeffs", Kind = FoamResponseKind.ForceCoeffs, Patches = ["body"],
+ Parameters = [new FoamNamedValueData { Name = "magUInf", Value = value }]
+ }
+ ]
+ };
+
+ Assert.That(FoamResponseRules.Validate(Coefficients("{{oc1}}")), Is.Empty, "a speed swept with the case");
+ Assert.That(FoamResponseRules.Validate(Coefficients("({{oc1}} 0 0)")), Is.Empty, "a placeholder inside a vector");
+ Assert.That(FoamResponseRules.Validate(Coefficients("{{oc1}};#calc")), Has.Some.Contains("the value of 'magUInf' is not a plain dictionary value"));
+ Assert.That(FoamResponseRules.Validate(Coefficients("{{name}}")), Has.Some.Contains("not a plain dictionary value"), "only the sweep's own token shape");
+ }
+
#endregion
}
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamTemplatingTests.cs b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamTemplatingTests.cs
index 077f647..83041d4 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamTemplatingTests.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Model/Rules/FoamTemplatingTests.cs
@@ -179,5 +179,26 @@ public void AStudyWithoutTokensOverAnUntemplatedCaseIsCoveredTest()
Assert.That(FoamTemplating.CheckCoverage([], []), Is.Empty);
}
+ [Test]
+ public void AResponseTokenTheStudyDoesNotDeclareIsAFindingTest()
+ {
+ var request = new FoamExtractionRequestData
+ {
+ Responses =
+ [
+ new FoamResponseSpecData
+ {
+ Name = "coeffs", Kind = FoamResponseKind.ForceCoeffs, Patches = ["body"],
+ Parameters = [new FoamNamedValueData { Name = "magUInf", Value = "{{oc1}}" }, new FoamNamedValueData { Name = "lRef", Value = "{{oc3}}" }]
+ }
+ ]
+ };
+
+ Assert.That(FoamTemplating.CheckResponseCoverage(["{{oc1}}", "{{oc3}}"], request), Is.Empty);
+ Assert.That(FoamTemplating.CheckResponseCoverage(["{{oc1}}"], request),
+ Is.EqualTo(new[] { "Response 'coeffs': token {{oc3}} is not declared by the study." }));
+ Assert.That(FoamTemplating.CheckResponseCoverage(["{{oc1}}"], null), Is.Empty);
+ }
+
#endregion
}
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Oracle/FoamKitOracleTests.cs b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Oracle/FoamKitOracleTests.cs
index df75454..dc76f22 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Oracle/FoamKitOracleTests.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM.Tests/Oracle/FoamKitOracleTests.cs
@@ -115,6 +115,34 @@ private FoamTaskData PitzDaily(int threads, bool parallel)
};
}
+ ///
+ /// The task with force coefficients on the lower wall measured after the
+ /// solve, the reference speed given as written (a number or a token).
+ ///
+ private static FoamTaskData WithCoefficients(FoamTaskData task, string magUInf)
+ {
+ task.Case!.Extraction!.Responses.Add(new FoamResponseSpecData
+ {
+ Name = "coeffs",
+ Kind = FoamResponseKind.ForceCoeffs,
+ Patches = ["lowerWall"],
+ Parameters =
+ [
+ new FoamNamedValueData { Name = "rho", Value = "rhoInf" },
+ new FoamNamedValueData { Name = "rhoInf", Value = "1" },
+ new FoamNamedValueData { Name = "CofR", Value = "(0 0 0)" },
+ new FoamNamedValueData { Name = "liftDir", Value = "(0 1 0)" },
+ new FoamNamedValueData { Name = "dragDir", Value = "(1 0 0)" },
+ new FoamNamedValueData { Name = "pitchAxis", Value = "(0 0 1)" },
+ new FoamNamedValueData { Name = "magUInf", Value = magUInf },
+ new FoamNamedValueData { Name = "lRef", Value = "1" },
+ new FoamNamedValueData { Name = "Aref", Value = "1" }
+ ]
+ });
+ task.Case.Recipe!.Steps.Add(new FoamStepData { Utility = "simpleFoam", Arguments = ["-postProcess", "-func", "coeffs", "-latestTime"] });
+ return task;
+ }
+
///
/// The first response value whose name starts with the prefix. The column
/// names are the function objects' own (areaAverage(p),
@@ -196,6 +224,26 @@ public async Task PitzDailyRunsOnTwoRanksAndAgreesWithTheSerialRunTest()
Assert.That(Value(parallel, "pRange.", "max"), Is.EqualTo(Value(serial, "pRange.", "max")).Within(2).Percent);
}
+ [Test]
+ public async Task AForceCoefficientFollowsTheSweptReferenceSpeedTest()
+ {
+ var session = new FoamCaseSession(m_kit, m_blobs, new WitTempStorageDefault(m_storage));
+
+ // The same flow twice; the coefficients' reference speed written as
+ // 10, then taken from the variant as 20. OpenFOAM divides the same
+ // forces by the square of the reference: a quarter of the first Cd.
+ var written = await session.RunAsync(WithCoefficients(PitzDaily(threads: 1, parallel: false), "10"));
+ var followed = WithCoefficients(PitzDaily(threads: 1, parallel: false), "{{oc2}}");
+ followed.Substitutions.Add(new FoamTokenValueData { Token = "{{oc2}}", Value = "20" });
+ var swept = await session.RunAsync(followed);
+ PrintRow(written);
+ PrintRow(swept);
+
+ Assert.That(swept.Rejections, Is.Empty);
+ Assert.That(swept.ExitCode, Is.EqualTo(0), swept.LogTail);
+ Assert.That(Value(swept, "coeffs.", "Cd"), Is.EqualTo(Value(written, "coeffs.", "Cd") / 4).Within(0.01).Percent);
+ }
+
[Test]
public async Task ADivergingVariantComesBackAsAFailedResultTest()
{
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM/Extraction/FoamFunctionObjectWriter.cs b/OpenFOAM/OutWit.Controller.OpenFOAM/Extraction/FoamFunctionObjectWriter.cs
index d701914..252d9b7 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM/Extraction/FoamFunctionObjectWriter.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM/Extraction/FoamFunctionObjectWriter.cs
@@ -23,13 +23,24 @@ public static class FoamFunctionObjectWriter
///
/// The materialised case.
/// The request; null writes nothing.
+ /// The variant's values: a parameter that names a token (magUInf {{oc1}}) takes its value.
/// Findings, one sentence each; nothing is written when there is any.
- public static IReadOnlyList Write(string caseDirectory, FoamExtractionRequestData? request)
+ public static IReadOnlyList Write(string caseDirectory, FoamExtractionRequestData? request, IReadOnlyList? substitutions = null)
{
if (request == null || request.Responses.Count == 0)
return [];
+ // A parameter that follows a swept value takes this variant's value;
+ // the rules then judge the value itself, not the placeholder.
+ request = Instantiate(request, substitutions ?? []);
+
var findings = FoamResponseRules.Validate(request).ToList();
+ foreach (var response in request.Responses)
+ {
+ foreach (var token in response.Parameters.SelectMany(parameter => FoamTemplating.LeftoverTokens(parameter.Value)).Distinct(StringComparer.Ordinal))
+ findings.Add($"Response '{response.Name}': token {token} has no value in this variant.");
+ }
+
var system = Path.Combine(caseDirectory, "system");
// A response never overwrites a file the user shipped under the same name.
@@ -110,6 +121,15 @@ public static string Render(FoamResponseSpecData response)
return text.ToString();
}
+ private static FoamExtractionRequestData Instantiate(FoamExtractionRequestData request, IReadOnlyList substitutions)
+ {
+ var instantiated = request.Clone();
+ foreach (var parameter in instantiated.Responses.SelectMany(response => response.Parameters))
+ parameter.Value = FoamTemplating.Substitute(parameter.Value, substitutions);
+
+ return instantiated;
+ }
+
private static string List(string keyword, IReadOnlyList items)
{
return $"{keyword,-16}({string.Join(' ', items)});\n";
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM/OutWit.Controller.OpenFOAM.csproj b/OpenFOAM/OutWit.Controller.OpenFOAM/OutWit.Controller.OpenFOAM.csproj
index d7e17c5..66a457f 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM/OutWit.Controller.OpenFOAM.csproj
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM/OutWit.Controller.OpenFOAM.csproj
@@ -4,7 +4,7 @@
OpenFOAM
- 1.0.4
+ 1.0.5
Runs complete OpenFOAM cases on compute nodes: each run is one whole case on one node - meshing, decomposition, the solver, the post step - through an allow-listed recipe, the unit of parameter-study throughput. Carries pinned OpenFOAM v2606 kits for every supported node platform as controller assets, so nodes need no preinstalled OpenFOAM (parallel steps on Windows additionally need Microsoft MPI on the node); responses are read from the finished case right on the node. OpenFOAM is a registered trademark of OpenCFD Limited; this offering is not approved or endorsed by OpenCFD Limited.
openfoam;cfd;solver;simulation;witengine;witcloud;controller
foam-run
diff --git a/OpenFOAM/OutWit.Controller.OpenFOAM/Runtime/FoamCaseSession.cs b/OpenFOAM/OutWit.Controller.OpenFOAM/Runtime/FoamCaseSession.cs
index 3b115fe..9adf60e 100644
--- a/OpenFOAM/OutWit.Controller.OpenFOAM/Runtime/FoamCaseSession.cs
+++ b/OpenFOAM/OutWit.Controller.OpenFOAM/Runtime/FoamCaseSession.cs
@@ -75,7 +75,7 @@ public async Task RunAsync(FoamTaskData task, CancellationToken
if (rejections.Count == 0)
{
rejections.AddRange(await FoamCaseMaterializer.MaterializeAsync(task, caseDirectory, BlobService, cancellationToken));
- rejections.AddRange(FoamFunctionObjectWriter.Write(caseDirectory, task.Case?.Extraction));
+ rejections.AddRange(FoamFunctionObjectWriter.Write(caseDirectory, task.Case?.Extraction, task.Substitutions));
}
if (rejections.Count == 0)
diff --git a/Sweep/OutWit.Controller.Sweep.Tests/Families/SweepFamilyOpenFOAMTests.cs b/Sweep/OutWit.Controller.Sweep.Tests/Families/SweepFamilyOpenFOAMTests.cs
index 7440692..a65802f 100644
--- a/Sweep/OutWit.Controller.Sweep.Tests/Families/SweepFamilyOpenFOAMTests.cs
+++ b/Sweep/OutWit.Controller.Sweep.Tests/Families/SweepFamilyOpenFOAMTests.cs
@@ -93,6 +93,28 @@ public async Task TokenCoverageIsCheckedOverTheTemplatedFilesOnlyTest()
}));
}
+ [Test]
+ public async Task AResponseMayFollowTheStudysTokensAndNoOtherTest()
+ {
+ var options = Study();
+ options.OpenFOAM!.Extraction = new FoamExtractionRequestData
+ {
+ Responses =
+ [
+ new FoamResponseSpecData
+ {
+ Name = "coeffs", Kind = FoamResponseKind.ForceCoeffs, Patches = ["body"],
+ Parameters = [new FoamNamedValueData { Name = "magUInf", Value = "{{oc1}}" }]
+ }
+ ]
+ };
+
+ Assert.That(await m_family.ValidateAsync(options, m_blobs), Is.Empty, "the speed's own token");
+
+ options.OpenFOAM.Extraction.Responses[0].Parameters[0].Value = "{{oc7}}";
+ Assert.That(await m_family.ValidateAsync(options, m_blobs), Is.EqualTo(new[] { "Response 'coeffs': token {{oc7}} is not declared by the study." }));
+ }
+
[Test]
public async Task AStudyWithoutItsBlockIsRefusedTest()
{
diff --git a/Sweep/OutWit.Controller.Sweep/Families/SweepFamilyOpenFOAM.cs b/Sweep/OutWit.Controller.Sweep/Families/SweepFamilyOpenFOAM.cs
index 83b7ab2..2c45011 100644
--- a/Sweep/OutWit.Controller.Sweep/Families/SweepFamilyOpenFOAM.cs
+++ b/Sweep/OutWit.Controller.Sweep/Families/SweepFamilyOpenFOAM.cs
@@ -52,7 +52,9 @@ public async Task> ValidateAsync(SweepOptionsData options,
templated.Add((file.RelativePath, FoamCaseText.FromBytes(await File.ReadAllBytesAsync(path))));
}
- findings.AddRange(FoamTemplating.CheckCoverage(options.Parameters.Select(parameter => parameter.Token).ToList(), templated));
+ var tokens = options.Parameters.Select(parameter => parameter.Token).ToList();
+ findings.AddRange(FoamTemplating.CheckCoverage(tokens, templated));
+ findings.AddRange(FoamTemplating.CheckResponseCoverage(tokens, data.Extraction));
return findings;
}
diff --git a/Sweep/OutWit.Controller.Sweep/OutWit.Controller.Sweep.csproj b/Sweep/OutWit.Controller.Sweep/OutWit.Controller.Sweep.csproj
index bb70a36..7a073e3 100644
--- a/Sweep/OutWit.Controller.Sweep/OutWit.Controller.Sweep.csproj
+++ b/Sweep/OutWit.Controller.Sweep/OutWit.Controller.Sweep.csproj
@@ -4,7 +4,7 @@
Sweep
- 2.0.3
+ 2.0.4
Host-side orchestration of parameter studies for every solver family the host knows - CalculiX decks and OpenFOAM cases - with one activity vocabulary: validates the study and its family block up front, fans each chunk out with Grid.ForEach to the family's node activity, and harvests the results into a manifest blob after every chunk, each row the sweep's verdict and the node's own result, so partial results are first-class and progress is observable while the sweep runs. Progressive chunk sizing: the first chunk as wide as the fleet, geometric growth to a cap.
sweep;parameter-study;doe;calculix;ccx;openfoam;cfd;witengine;witcloud;controller
sweep-solve