diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs
index 7d63fe6f..e92ef2f1 100644
--- a/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs
+++ b/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs
@@ -257,6 +257,18 @@ public void RealRun_SimulatesAFrozenCopy_AndSavesAResultModelWithOnlyOpenStudioR
Assert.NotEmpty(results);
Assert.All(results, x => Assert.Equal(AnalyticalModelSimulationResult.OpenStudioSource, x.Source));
Assert.Single(result.GetAnalyticalModelSimulationResults());
+
+ //S03: the provenance of a real run names what actually ran - the CLI by hash, EnergyPlus by its own eplusout.err.
+ JsonObject provenance = JsonNode.Parse(File.ReadAllText(run.ManifestPath))["provenance"].AsObject();
+ JsonObject engine = provenance["engine"].AsObject();
+ Assert.Equal(Query.Sha256(run.CliPath), (string)engine["cli"]["sha256"]);
+ Assert.Equal(SimulationRunProvenance.EnergyPlusVersionFromErr(run.ErrorFilePath), (string)engine["energyPlusVersion"]);
+ Assert.NotEqual(SimulationRunProvenance.Unknown, (string)engine["energyPlusVersion"]);
+ Assert.NotEqual(SimulationRunProvenance.Unknown, (string)engine["openStudioSdkVersion"]);
+ Assert.Equal(run.DesignDaySource, (string)provenance["designDays"]["source"]);
+ Assert.Equal(epwPath == null ? "Embedded" : "File", (string)provenance["weather"]["source"]);
+ Assert.Equal(run.WeatherSha256, (string)provenance["weather"]["sha256"]);
+ Assert.False((bool)provenance["settings"]["runOptions"]["useUniqueRunDirectory"]);
}
}
}
diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/ProcessEnvironmentCollection.cs b/WPF/SAM.Analytical.UI.WPF.Tests/ProcessEnvironmentCollection.cs
new file mode 100644
index 00000000..46c7303c
--- /dev/null
+++ b/WPF/SAM.Analytical.UI.WPF.Tests/ProcessEnvironmentCollection.cs
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
+
+using Xunit;
+
+namespace SAM.Analytical.UI.WPF.Tests
+{
+ ///
+ /// The xUnit collection for classes that set process environment variables (SAM_COMMIT, say), which every other test and every
+ /// child process would otherwise see. It never runs in parallel with anything else.
+ ///
+ [CollectionDefinition(Name, DisableParallelization = true)]
+ public sealed class ProcessEnvironmentCollection
+ {
+ public const string Name = "Process environment";
+ }
+}
diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/SimulationRunProvenanceTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/SimulationRunProvenanceTests.cs
new file mode 100644
index 00000000..88be1ec8
--- /dev/null
+++ b/WPF/SAM.Analytical.UI.WPF.Tests/SimulationRunProvenanceTests.cs
@@ -0,0 +1,802 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
+
+#nullable enable
+
+using SAM.Analytical.Tas;
+using SAM.Weather;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Nodes;
+using System.Threading;
+using Xunit;
+
+namespace SAM.Analytical.UI.WPF.Tests
+{
+ ///
+ /// S03: every Tas, OpenStudio and Both run manifest carries a provenance block - the build, the engine and producer binaries, the
+ /// weather, the design days and the settings actually given - filled from what ran, never from a manual SAM_COMMIT or a Git HEAD, and
+ /// with every unknown item named with its reason. No engine runs here: producers are faked as in ,
+ /// the Tas route's record is driven through its own writer, and a real OpenStudio run is the existing RealRun test.
+ ///
+ [Collection(ProcessEnvironmentCollection.Name)]
+ public class SimulationRunProvenanceTests : IDisposable
+ {
+ private readonly string directory;
+
+ public SimulationRunProvenanceTests()
+ {
+ directory = Path.Combine(Path.GetTempPath(), "SAM-UI-S03-tests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ foreach (string path in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories))
+ {
+ File.SetAttributes(path, FileAttributes.Normal);
+ }
+
+ Directory.Delete(directory, true);
+ }
+ catch (IOException)
+ {
+ }
+ catch (UnauthorizedAccessException)
+ {
+ }
+ }
+
+ // ---------------------------------------------------------------- the build
+
+ [Theory]
+ [InlineData("2026.4.12.0+1bee804f", "1bee804f")]
+ [InlineData("2026.4.12.0+1BEE804F", "1bee804f")]
+ [InlineData("1.0.0+0123456789abcdef0123456789abcdef01234567", "0123456789abcdef0123456789abcdef01234567")]
+ [InlineData("1.0.0", null)]
+ [InlineData("1.0.0+unknown", null)]
+ [InlineData("1.0.0+abc", null)]
+ [InlineData(null, null)]
+ public void SourceRevision_IsOnlyTheBuildStamp(string? informationalVersion, string? expected)
+ {
+ Assert.Equal(expected, SimulationRunProvenance.SourceRevision(informationalVersion));
+ }
+
+ ///
+ /// The build is identified by its own stamp and its binaries' hashes. A manual SAM_COMMIT is not read, and an unstamped (developer)
+ /// build says "unknown" with its reason - even though it is built from a Git checkout whose HEAD could have been read.
+ ///
+ [Fact]
+ public void Application_IsIdentifiedByItsStampAndHashes_NeverBySamCommitOrGit()
+ {
+ string? samCommit = Environment.GetEnvironmentVariable("SAM_COMMIT");
+ Environment.SetEnvironmentVariable("SAM_COMMIT", "deadbeefdeadbeef");
+ JsonObject provenance;
+ try
+ {
+ provenance = new SimulationRunProvenance("Tas").ToJsonObject();
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("SAM_COMMIT", samCommit);
+ }
+
+ JsonObject application = provenance["application"]!.AsObject();
+ string? stamp = SimulationRunProvenance.SourceRevision(SimulationRunProvenance.InformationalVersion(typeof(SimulationRunProvenance).Assembly));
+ if (stamp == null)
+ {
+ Assert.Equal(SimulationRunProvenance.Unknown, (string)application["sourceRevision"]!);
+ Assert.Contains("local developer build", (string)provenance["unknown"]!["application.sourceRevision"]!);
+ }
+ else
+ {
+ Assert.Equal(stamp, (string)application["sourceRevision"]!);
+ }
+
+ Assert.DoesNotContain("deadbeef", provenance.ToJsonString());
+
+ Dictionary components = application["components"]!.AsArray().Select(x => x!.AsObject()).ToDictionary(x => (string)x["name"]!);
+ foreach (Type type in new[] { typeof(Core.SAMObject), typeof(AnalyticalModel), typeof(SimulationRunProvenance) })
+ {
+ JsonObject component = components[type.Assembly.GetName().Name!];
+ Assert.Equal(type.Assembly.Location, (string)component["path"]!);
+ Assert.Equal(Query.Sha256(type.Assembly.Location), (string)component["sha256"]!);
+ Assert.Equal(type.Assembly.GetName().Version!.ToString(), (string)component["version"]!);
+ }
+
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ // ---------------------------------------------------------------- executables and engines
+
+ [Fact]
+ public void Executable_RecordsPathHashAndVersions_AndTheApphostAssembly()
+ {
+ string path = Process.GetCurrentProcess().MainModule!.FileName!;
+ SimulationRunProvenance provenance = new SimulationRunProvenance("Both");
+ SimulationRunProvenance.SetExecutable(provenance.Root.Section("executable"), path);
+
+ JsonObject executable = provenance.ToJsonObject()["executable"]!.AsObject();
+ FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(path);
+ Assert.Equal(path, (string)executable["path"]!);
+ Assert.Equal(Query.Sha256(path), (string)executable["sha256"]!);
+ Assert.Equal(fileVersionInfo.FileVersion, (string)executable["fileVersion"]!);
+ Assert.Equal(fileVersionInfo.ProductVersion, (string)executable["productVersion"]!);
+
+ string path_Assembly = Path.ChangeExtension(path, ".dll");
+ if (File.Exists(path_Assembly))
+ {
+ Assert.Equal(Query.Sha256(path_Assembly), (string)executable["assembly"]!["sha256"]!);
+ }
+ }
+
+ [Fact]
+ public void Executable_MissingOrVersionless_IsUnknownWithItsReason()
+ {
+ string path_Missing = Path.Combine(directory, "missing.exe");
+ string path_Versionless = Path.Combine(directory, "versionless.exe");
+ File.WriteAllText(path_Versionless, "not a PE file");
+
+ SimulationRunProvenance provenance = new SimulationRunProvenance("Both");
+ SimulationRunProvenance.SetExecutable(provenance.Root.Section("none"), null);
+ SimulationRunProvenance.SetExecutable(provenance.Root.Section("missing"), path_Missing);
+ SimulationRunProvenance.SetExecutable(provenance.Root.Section("versionless"), path_Versionless);
+ JsonObject json = provenance.ToJsonObject();
+
+ Assert.Equal("unknown", (string)json["none"]!["path"]!);
+ Assert.Contains("no executable", (string)json["unknown"]!["none.path"]!);
+ Assert.Equal("unknown", (string)json["missing"]!["sha256"]!);
+ Assert.Contains("does not exist", (string)json["unknown"]!["missing.sha256"]!);
+ Assert.Equal(Query.Sha256(path_Versionless), (string)json["versionless"]!["sha256"]!);
+ Assert.Equal("unknown", (string)json["versionless"]!["fileVersion"]!);
+ Assert.Contains("no version resource", (string)json["unknown"]!["versionless.fileVersion"]!);
+ AssertEveryUnknownHasAReason(json);
+ }
+
+ [Theory]
+ [InlineData("\"C:\\Program Files\\Tas\\TBD.exe\" /Automation", "C:\\Program Files\\Tas\\TBD.exe")]
+ [InlineData("C:\\NOTHERE~1\\NOTHERE~2\\Tas\\TBD.exe /Automation", "C:\\NOTHERE~1\\NOTHERE~2\\Tas\\TBD.exe")]
+ [InlineData("C:\\Tas\\TBD.exe", "C:\\Tas\\TBD.exe")]
+ [InlineData("", null)]
+ [InlineData(null, null)]
+ public void CommandPath_IsTheRegisteredExecutable(string? command, string? expected)
+ {
+ Assert.Equal(expected, SimulationRunProvenance.CommandPath(command));
+ }
+
+ [Fact]
+ public void TasEngine_IsTheRegisteredComServer_WithItsVersionAndHash()
+ {
+ string path = Process.GetCurrentProcess().MainModule!.FileName!;
+ SimulationRunProvenance provenance = new SimulationRunProvenance("Tas");
+ SimulationRunProvenance.SetTasEngine(provenance.Root.Section("engine"), "\"" + path + "\" -Embedding");
+ JsonObject engine = provenance.ToJsonObject()["engine"]!.AsObject();
+
+ Assert.Equal("TBD.Document", (string)engine["comServer"]!);
+ Assert.Equal(FileVersionInfo.GetVersionInfo(path).ProductVersion, (string)engine["version"]!);
+ Assert.Equal(Query.Sha256(path), (string)engine["executable"]!["sha256"]!);
+ }
+
+ [Fact]
+ public void TasEngine_NotRegisteredOrVersionless_IsUnknownWithItsReason()
+ {
+ string path_Versionless = Path.Combine(directory, "TBD.exe");
+ File.WriteAllText(path_Versionless, "not a PE file");
+
+ SimulationRunProvenance provenance = new SimulationRunProvenance("Tas");
+ SimulationRunProvenance.SetTasEngine(provenance.Root.Section("none"), null);
+ SimulationRunProvenance.SetTasEngine(provenance.Root.Section("versionless"), path_Versionless + " /Automation");
+ JsonObject json = provenance.ToJsonObject();
+
+ Assert.Equal("unknown", (string)json["none"]!["version"]!);
+ Assert.Contains("no TBD.Document COM server is registered", (string)json["unknown"]!["none.version"]!);
+ Assert.Equal("unknown", (string)json["versionless"]!["version"]!);
+ Assert.Contains("no product version", (string)json["unknown"]!["versionless.version"]!);
+ Assert.Equal(Query.Sha256(path_Versionless), (string)json["versionless"]!["executable"]!["sha256"]!);
+ AssertEveryUnknownHasAReason(json);
+ }
+
+ [Fact]
+ public void EnergyPlusVersion_IsTheOneInEplusoutErr()
+ {
+ string path = Path.Combine(directory, "eplusout.err");
+ File.WriteAllText(path, "Program Version,EnergyPlus, Version 25.2.0-cf7368216c, YMD=2026.10.19 10:57,\n ** Warning ** something\n");
+ Assert.Equal("25.2.0-cf7368216c", SimulationRunProvenance.EnergyPlusVersionFromErr(path));
+
+ File.WriteAllText(path, " ** Severe ** no version line\n");
+ Assert.Null(SimulationRunProvenance.EnergyPlusVersionFromErr(path));
+ Assert.Null(SimulationRunProvenance.EnergyPlusVersionFromErr(Path.Combine(directory, "none.err")));
+ Assert.Null(SimulationRunProvenance.EnergyPlusVersionFromErr(null));
+ }
+
+ /// A collector that fails part-way leaves its whole item unknown, with no reasons left for sub-items it had filed.
+ [Fact]
+ public void AFailedCollector_LeavesItsItemUnknown_WithNoStaleReasons()
+ {
+ SimulationRunProvenance provenance = new SimulationRunProvenance("Tas");
+ ProvenanceSection weather = provenance.Root.Section("weather");
+ SimulationRunProvenance.Guard(provenance.Root, "weather", () =>
+ {
+ weather.SetUnknown("file", "not read from a file");
+ throw new InvalidOperationException("half way");
+ });
+
+ JsonObject json = provenance.ToJsonObject();
+ Assert.Equal("unknown", (string)json["weather"]!);
+ Assert.Contains("half way", (string)json["unknown"]!["weather"]!);
+ Assert.Null(json["unknown"]!["weather.file"]);
+ AssertEveryUnknownHasAReason(json);
+ }
+
+ /// The weather's content hash ignores the object's own Guid (new on every read) and nothing else.
+ [Fact]
+ public void WeatherContentHash_IgnoresOnlyTheWeathersOwnGuid()
+ {
+ WeatherData weatherData = Weather("Read", 0.0);
+ JsonObject jsonObject = weatherData.ToJsonObject();
+ jsonObject["Guid"] = Guid.NewGuid().ToString();
+ WeatherData weatherData_ReadAgain = new WeatherData(jsonObject);
+
+ Assert.NotEqual(weatherData.Guid, weatherData_ReadAgain.Guid);
+ Assert.Equal(SimulationRunProvenance.WeatherContentSha256(weatherData), SimulationRunProvenance.WeatherContentSha256(weatherData_ReadAgain));
+ Assert.NotEqual(SimulationRunProvenance.WeatherContentSha256(weatherData), SimulationRunProvenance.WeatherContentSha256(Weather("Read", 0.5)));
+ }
+
+ // ---------------------------------------------------------------- Tas only
+
+ [Fact]
+ public void Tas_EmbeddedWeather_DerivedDesignDays_AndTheWorkflowSettingsThatRan()
+ {
+ WeatherData weatherData = Weather("Embedded", 0.0);
+ AnalyticalModel analyticalModel = Model(weatherData);
+ WorkflowSettings workflowSettings = WorkflowSettingsAsRunPartOSimulationBuildsThem(new WeatherData(weatherData), 1, 365);
+
+ JsonObject provenance = Query.TasRunProvenance(analyticalModel, new WeatherData(weatherData), null, workflowSettings, Inputs(), null).ToJsonObject();
+
+ Assert.Equal("Tas", (string)provenance["route"]!);
+ JsonObject weather = provenance["weather"]!.AsObject();
+ string contentSha256 = SimulationRunProvenance.WeatherContentSha256(weatherData);
+ Assert.Equal("Embedded", (string)weather["source"]!);
+ Assert.Equal("Embedded", (string)weather["name"]!);
+ Assert.Equal(contentSha256, (string)weather["contentSha256"]!);
+ Assert.Equal("unknown", (string)weather["file"]!);
+ Assert.Contains("embedded WeatherData", (string)provenance["unknown"]!["weather.file"]!);
+
+ JsonObject designDays = provenance["designDays"]!.AsObject();
+ Assert.Equal("DerivedFromWeather", (string)designDays["source"]!);
+ Assert.Single(designDays["heating"]!.AsArray());
+ Assert.Single(designDays["cooling"]!.AsArray());
+ Assert.True((bool)designDays["usedForSizing"]!);
+
+ //The settings that ran, with the weather identified rather than repeated.
+ JsonObject workflow = provenance["settings"]!["workflow"]!.AsObject();
+ Assert.Equal(365, (int)workflow["SimulateTo"]!);
+ Assert.Equal(1, (int)workflow["SimulateFrom"]!);
+ Assert.True((bool)workflow["Sizing"]!);
+ Assert.Equal(contentSha256, (string)workflow["WeatherData"]!["contentSha256"]!);
+ Assert.Null(workflow["WeatherData"]!["WeatherYears"]);
+ Assert.Equal("TAS", (string)provenance["settings"]!["inputs"]!["solarCalculationMethod"]!);
+
+ //No TAS here: the engine is unknown, with the reason.
+ Assert.Equal("unknown", (string)provenance["engine"]!["version"]!);
+ Assert.Contains("TBD.Document", (string)provenance["unknown"]!["engine.version"]!);
+ Assert.Contains(provenance["application"]!["components"]!.AsArray(), x => (string)x!["name"]! == typeof(WorkflowSettings).Assembly.GetName().Name);
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ [Fact]
+ public void Tas_ExplicitWeatherFromAFile_IsExplicit_WithTheFilesHash()
+ {
+ AnalyticalModel analyticalModel = Model(Weather("Embedded", 0.0));
+ WeatherData weatherData = Weather("Chosen", 5.0);
+ string path_Weather = Path.Combine(directory, "chosen.epw");
+ File.WriteAllText(path_Weather, "LOCATION,Chosen,,,TMY,000000,0,0,0,0\n");
+
+ JsonObject provenance = Query.TasRunProvenance(analyticalModel, weatherData, path_Weather, WorkflowSettingsAsRunPartOSimulationBuildsThem(weatherData, 1, 365), Inputs(), null).ToJsonObject();
+
+ JsonObject weather = provenance["weather"]!.AsObject();
+ Assert.Equal("Explicit", (string)weather["source"]!);
+ Assert.Equal(path_Weather, (string)weather["file"]!["path"]!);
+ Assert.Equal(Query.Sha256(path_Weather), (string)weather["file"]!["sha256"]!);
+ Assert.Equal(SimulationRunProvenance.WeatherContentSha256(weatherData), (string)weather["contentSha256"]!);
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ /// A weather labelled as the model's but not its content (a remembered one) is not embedded.
+ [Fact]
+ public void Tas_RememberedWeatherNotFromAFile_IsExplicit_AndItsFileUnknown()
+ {
+ AnalyticalModel analyticalModel = Model(Weather("Embedded", 0.0));
+ WeatherData weatherData = Weather("Embedded", 3.0);
+
+ JsonObject provenance = Query.TasRunProvenance(analyticalModel, weatherData, null, null, Inputs(), null).ToJsonObject();
+
+ Assert.Equal("Explicit", (string)provenance["weather"]!["source"]!);
+ Assert.Contains("remembered", (string)provenance["unknown"]!["weather.file"]!);
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ [Fact]
+ public void Tas_StoppedBeforeTheWorkflow_SaysSoForDesignDaysAndSettings_AndNoWeatherIsNone()
+ {
+ JsonObject provenance = Query.TasRunProvenance(Model(null), null, null, null, Inputs(), null).ToJsonObject();
+
+ Assert.Equal("None", (string)provenance["weather"]!["source"]!);
+ Assert.Equal("unknown", (string)provenance["designDays"]!["source"]!);
+ Assert.Contains("before the TAS workflow settings were built", (string)provenance["unknown"]!["designDays.source"]!);
+ Assert.Equal("unknown", (string)provenance["settings"]!["workflow"]!);
+ Assert.Contains("before the TAS workflow settings were built", (string)provenance["unknown"]!["settings.workflow"]!);
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ [Fact]
+ public void Tas_DesignDaysNotDerivedFromTheWeather_AreNotClaimedAsDerived()
+ {
+ WeatherData weatherData = Weather("Embedded", 0.0);
+ WorkflowSettings workflowSettings = WorkflowSettingsAsRunPartOSimulationBuildsThem(Weather("Other", 9.0), 1, 365);
+
+ JsonObject provenance = Query.TasRunProvenance(Model(weatherData), weatherData, null, workflowSettings, Inputs(), null).ToJsonObject();
+
+ Assert.Equal("unknown", (string)provenance["designDays"]!["source"]!);
+ Assert.Contains("not the ones derived", (string)provenance["unknown"]!["designDays.source"]!);
+ }
+
+ [Theory]
+ [InlineData(true, false, null, false, TasSimulationStatus.Succeeded)]
+ [InlineData(false, false, null, false, TasSimulationStatus.Failed)]
+ [InlineData(false, true, null, false, TasSimulationStatus.Cancelled)]
+ [InlineData(false, false, "no gbXML", false, TasSimulationStatus.Refused)]
+ [InlineData(true, false, null, true, TasSimulationStatus.Failed)]
+ [InlineData(false, true, null, true, TasSimulationStatus.Failed)]
+ public void TasStatus_IsTheWorkflowsOwnOutcome(bool returnedModel, bool cancelled, string? refusal, bool threw, TasSimulationStatus expected)
+ {
+ Assert.Equal(expected, TasSimulationRun.StatusOf(returnedModel, cancelled, refusal, threw ? new InvalidOperationException("COM") : null));
+ }
+
+ /// The Tas record of a failed run (S01b's failure text) is written beside the TBD with its provenance.
+ [Fact]
+ public void TasManifest_OfAFailedRun_IsWrittenBesideTheTbd_WithFailureAndProvenance()
+ {
+ WeatherData weatherData = Weather("Embedded", 0.0);
+ TasSimulationRun tasSimulationRun = TasRun(TasSimulationStatus.Failed);
+ tasSimulationRun.Failure = "TAS refused to simulate (return code 0); see the error log";
+
+ Modify.WriteTasSimulationManifest(tasSimulationRun, () => Query.TasRunProvenance(Model(weatherData), weatherData, null, WorkflowSettingsAsRunPartOSimulationBuildsThem(weatherData, 1, 365), Inputs(), null));
+
+ Assert.Equal(Path.Combine(directory, "Box" + TasSimulationRun.ManifestFileSuffix), tasSimulationRun.ManifestPath);
+ JsonObject manifest = JsonNode.Parse(File.ReadAllText(tasSimulationRun.ManifestPath!))!.AsObject();
+ Assert.Equal(TasSimulationRun.Schema, (string)manifest["schema"]!);
+ Assert.Equal("Failed", (string)manifest["status"]!);
+ Assert.Equal(tasSimulationRun.Failure, (string)manifest["failure"]!);
+ Assert.Equal(SimulationRunProvenance.Schema, (string)manifest["provenance"]!["schema"]!);
+ Assert.Equal("Embedded", (string)manifest["provenance"]!["weather"]!["source"]!);
+ AssertEveryUnknownHasAReason(manifest["provenance"]!.AsObject());
+ }
+
+ /// Collecting provenance that throws neither changes the run's status nor stops the record being written.
+ [Fact]
+ public void TasManifest_WhenProvenanceThrows_KeepsTheStatus_AndIsStillWritten()
+ {
+ TasSimulationRun tasSimulationRun = TasRun(TasSimulationStatus.Succeeded);
+
+ Modify.WriteTasSimulationManifest(tasSimulationRun, () => throw new InvalidOperationException("registry unavailable"));
+
+ Assert.Equal(TasSimulationStatus.Succeeded, tasSimulationRun.Status);
+ JsonObject manifest = JsonNode.Parse(File.ReadAllText(tasSimulationRun.ManifestPath!))!.AsObject();
+ Assert.Equal("Succeeded", (string)manifest["status"]!);
+ Assert.Contains(manifest["notes"]!.AsArray(), x => ((string)x!).Contains("registry unavailable"));
+ }
+
+ [Fact]
+ public void TasManifest_OfACancelledOrThrowingRun_RecordsIt_AndAMissingFolderWritesNothing()
+ {
+ TasSimulationRun cancelled = TasRun(TasSimulationStatus.Cancelled);
+ Modify.WriteTasSimulationManifest(cancelled, () => new SimulationRunProvenance("Tas"));
+ Assert.Equal("Cancelled", (string)JsonNode.Parse(File.ReadAllText(cancelled.ManifestPath!))!["status"]!);
+
+ TasSimulationRun threw = TasRun(TasSimulationStatus.Failed);
+ threw.ProjectName = "Threw";
+ threw.Exception = new System.Runtime.InteropServices.COMException("TBD server died");
+ Modify.WriteTasSimulationManifest(threw, () => new SimulationRunProvenance("Tas"));
+ JsonObject manifest = JsonNode.Parse(File.ReadAllText(threw.ManifestPath!))!.AsObject();
+ Assert.Equal("Failed", (string)manifest["status"]!);
+ Assert.Contains("TBD server died", (string)manifest["error"]!);
+
+ TasSimulationRun missing = TasRun(TasSimulationStatus.Refused);
+ missing.Directory = Path.Combine(directory, "never-created");
+ Modify.WriteTasSimulationManifest(missing, () => new SimulationRunProvenance("Tas"));
+ Assert.Null(missing.ManifestPath);
+ Assert.False(Directory.Exists(missing.Directory));
+ }
+
+ // ---------------------------------------------------------------- Both
+
+ [Fact]
+ public void Both_RecordsEachProducersBinary_AndWhatEachReported_WithoutSamCommit()
+ {
+ string? samCommit = Environment.GetEnvironmentVariable("SAM_COMMIT");
+ Environment.SetEnvironmentVariable("SAM_COMMIT", null);
+ EngineComparisonRun run;
+ string tasExe = ProducerPath("benchmark-tas.exe", "tas binary");
+ string openStudioExe = ProducerPath("benchmark-openstudio.exe", "openstudio binary");
+ try
+ {
+ run = Modify.RunEngineComparison(EngineModel(), directory, Epw(), tasExe, openStudioExe, null, null, CancellationToken.None, (producerRun, workDirectory, cancellationToken) =>
+ {
+ File.Copy(producerRun.Engine == ComparisonEngine.Tas ? TasDocument : OpenStudioDocument, Argument(producerRun, "--out"));
+ return 0;
+ });
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("SAM_COMMIT", samCommit);
+ }
+
+ Assert.Equal(EngineComparisonRunStatus.Compared, run.Status);
+ JsonObject provenance = Manifest(run.ManifestPath)["provenance"]!.AsObject();
+ Assert.Equal("Both", (string)provenance["route"]!);
+
+ JsonObject tas = provenance["producers"]!["Tas"]!.AsObject();
+ Assert.Equal(Query.Sha256(tasExe), (string)tas["executable"]!["sha256"]!);
+ Assert.Equal("--model", (string)tas["arguments"]![0]!);
+ Assert.True((bool)tas["succeeded"]!);
+ JsonObject document = JsonNode.Parse(File.ReadAllText(TasDocument))!["provenance"]!.AsObject();
+ Assert.Equal((string)document["samCommit"]!, (string)tas["reported"]!["samCommit"]!);
+ Assert.Equal((string)document["runnerCommit"]!, (string)tas["reported"]!["runnerCommit"]!);
+ Assert.Equal((string)document["designDaySource"]!, (string)tas["reported"]!["designDaySource"]!);
+ Assert.Equal(document["engine"]!["kind"]!.ToJsonString(), tas["reported"]!["engine"]!["kind"]!.ToJsonString());
+
+ Assert.Equal(Query.Sha256(openStudioExe), (string)provenance["producers"]!["OpenStudio"]!["executable"]!["sha256"]!);
+ Assert.Equal("not set", (string)provenance["environment"]!["SAM_COMMIT"]!);
+ Assert.Equal("File", (string)provenance["weather"]!["source"]!);
+ Assert.Equal(run.WeatherSha256, (string)provenance["weather"]!["sha256"]!);
+ Assert.Contains(provenance["application"]!["components"]!.AsArray(), x => (string)x!["name"]! == "SAM.Analytical.Benchmark.Compare");
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ /// A manual commit override the producers inherit is recorded as such: the reported commits came from it.
+ [Fact]
+ public void Both_RecordsAManualSamCommitOverride()
+ {
+ string? samCommit = Environment.GetEnvironmentVariable("SAM_COMMIT");
+ Environment.SetEnvironmentVariable("SAM_COMMIT", "0123456789abcdef");
+ try
+ {
+ EngineComparisonRun run = Modify.RunEngineComparison(EngineModel(), directory, Epw(), ProducerPath("benchmark-tas.exe"), ProducerPath("benchmark-openstudio.exe"), null, null, CancellationToken.None, (producerRun, workDirectory, cancellationToken) => 5);
+ Assert.Equal("0123456789abcdef", (string)Manifest(run.ManifestPath)["provenance"]!["environment"]!["SAM_COMMIT"]!);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("SAM_COMMIT", samCommit);
+ }
+ }
+
+ /// S01b's regression shapes: a failed Tas document whose provenance is not an object, and the run still completes OpenStudio.
+ [Theory]
+ [InlineData("{\"provenance\":[]}")]
+ [InlineData("{\"provenance\":\"text\"}")]
+ [InlineData("{\"provenance\":42}")]
+ [InlineData("[]")]
+ [InlineData("{not json")]
+ public void Both_WithAMalformedFailedTasDocument_RecordsItsProvenanceAsUnknown_AndKeepsOpenStudio(string json)
+ {
+ EngineComparisonRun run = Modify.RunEngineComparison(EngineModel(), directory, Epw(), ProducerPath("benchmark-tas.exe"), ProducerPath("benchmark-openstudio.exe"), null, null, CancellationToken.None, (producerRun, workDirectory, cancellationToken) =>
+ {
+ if (producerRun.Engine == ComparisonEngine.Tas)
+ {
+ File.WriteAllText(Argument(producerRun, "--out"), json);
+ return 5;
+ }
+
+ File.Copy(OpenStudioDocument, Argument(producerRun, "--out"));
+ return 0;
+ });
+
+ Assert.Equal(EngineComparisonRunStatus.PartiallyCompleted, run.Status);
+ JsonObject provenance = Manifest(run.ManifestPath)["provenance"]!.AsObject();
+ Assert.Equal("unknown", (string)provenance["producers"]!["Tas"]!["reported"]!);
+ Assert.False((bool)provenance["producers"]!["Tas"]!["succeeded"]!);
+ Assert.NotNull(provenance["unknown"]!["producers.Tas.reported"]);
+ Assert.True(provenance["producers"]!["OpenStudio"]!["reported"] is JsonObject);
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ [Fact]
+ public void Both_WithAProducerNotFound_SaysItDidNotRun()
+ {
+ EngineComparisonRun run = Modify.RunEngineComparison(EngineModel(), directory, Epw(), ProducerPath("benchmark-tas.exe"), Path.Combine(directory, "missing", "benchmark-openstudio.exe"), null, null, CancellationToken.None, (producerRun, workDirectory, cancellationToken) =>
+ {
+ File.Copy(TasDocument, Argument(producerRun, "--out"));
+ return 0;
+ });
+
+ JsonObject provenance = Manifest(run.ManifestPath)["provenance"]!.AsObject();
+ Assert.Equal("unknown", (string)provenance["producers"]!["OpenStudio"]!["executable"]!);
+ Assert.Contains("did not run", (string)provenance["unknown"]!["producers.OpenStudio.executable"]!);
+ Assert.Contains("wrote no B1a document", (string)provenance["unknown"]!["producers.OpenStudio.reported"]!);
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ [Fact]
+ public void Both_CancelledOrThrowing_StillWritesItsProvenance()
+ {
+ using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
+ EngineComparisonRun cancelled = Modify.RunEngineComparison(EngineModel(), directory, Epw(), ProducerPath("benchmark-tas.exe"), ProducerPath("benchmark-openstudio.exe"), null, null, cancellationTokenSource.Token, (producerRun, workDirectory, cancellationToken) =>
+ {
+ cancellationTokenSource.Cancel();
+ cancellationToken.ThrowIfCancellationRequested();
+ return 0;
+ });
+
+ Assert.Equal(EngineComparisonRunStatus.Cancelled, cancelled.Status);
+ JsonObject provenance = Manifest(cancelled.ManifestPath)["provenance"]!.AsObject();
+ Assert.True(provenance["producers"]!["Tas"]!["executable"] is JsonObject);
+ AssertEveryUnknownHasAReason(provenance);
+
+ EngineComparisonRun threw = Modify.RunEngineComparison(EngineModel(), directory, Epw(), ProducerPath("benchmark-tas.exe"), ProducerPath("benchmark-openstudio.exe"), null, null, CancellationToken.None, (producerRun, workDirectory, cancellationToken) => throw new IOException("disk full"));
+
+ Assert.Equal(EngineComparisonRunStatus.Failed, threw.Status);
+ JsonObject provenance_Threw = Manifest(threw.ManifestPath)["provenance"]!.AsObject();
+ Assert.False((bool)provenance_Threw["producers"]!["Tas"]!["succeeded"]!);
+ AssertEveryUnknownHasAReason(provenance_Threw);
+ }
+
+ // ---------------------------------------------------------------- OpenStudio only
+
+ ///
+ /// A model with no weather and no EPW: the run fails (blocked by SAM_OpenStudio, or the stack cannot load). Its provenance says what
+ /// did not happen, with reasons, and the outcome is exactly what it was before S03.
+ ///
+ [Fact]
+ public void OpenStudio_FailedRun_RecordsWhatDidNotHappen_WithReasons()
+ {
+ OpenStudioSimulationRun run = Model(null).RunOpenStudioSimulation(directory, null, @"C:\models\source.json");
+
+ Assert.Equal(OpenStudioSimulationStatus.Failed, run.Status);
+ JsonObject provenance = Manifest(run.ManifestPath)["provenance"]!.AsObject();
+ Assert.Equal("OpenStudio", (string)provenance["route"]!);
+
+ //"None" where SAM_OpenStudio loaded and found no weather; unknown (with its reason) where the stack could not load at all.
+ Assert.Contains((string)provenance["weather"]!["source"]!, new[] { "None", SimulationRunProvenance.Unknown });
+ Assert.Equal("unknown", (string)provenance["weather"]!["sha256"]!);
+ Assert.Equal("unknown", (string)provenance["engine"]!["cli"]!);
+ Assert.Contains("not started", (string)provenance["unknown"]!["engine.cli"]!);
+ Assert.Equal("unknown", (string)provenance["engine"]!["energyPlusVersion"]!);
+ AssertEveryUnknownHasAReason(provenance);
+ }
+
+ // ---------------------------------------------------------------- historical manifests
+
+ ///
+ /// A manifest written before S03 (committed S01b evidence) has every key, with the same JSON kind, that a manifest written now has
+ /// outside the new provenance block: the change is additive, and a reader of the old shape reads the new one.
+ ///
+ [Fact]
+ public void Both_NewManifest_KeepsEveryHistoricalKey_AndOnlyAddsProvenance()
+ {
+ JsonObject historical = Manifest(Evidence("s01b-20261019", "cases", "B2-cand-both-success", "engine-comparison-run.json"));
+ Assert.Null(historical["provenance"]);
+
+ EngineComparisonRun run = Modify.RunEngineComparison(EngineModel(), directory, Epw(), ProducerPath("benchmark-tas.exe"), ProducerPath("benchmark-openstudio.exe"), @"C:\models\source.json", null, CancellationToken.None, (producerRun, workDirectory, cancellationToken) =>
+ {
+ File.Copy(producerRun.Engine == ComparisonEngine.Tas ? TasDocument : OpenStudioDocument, Argument(producerRun, "--out"));
+ return 0;
+ });
+ JsonObject current = Manifest(run.ManifestPath);
+
+ Assert.Equal((string)historical["schema"]!, (string)current["schema"]!);
+ Assert.Equal(historical.Select(x => x.Key).Append("provenance").OrderBy(x => x), current.Select(x => x.Key).OrderBy(x => x));
+ foreach (KeyValuePair keyValuePair in historical)
+ {
+ Assert.Equal(keyValuePair.Value?.GetValueKind(), current[keyValuePair.Key]?.GetValueKind());
+ }
+
+ JsonObject producer_Historical = historical["producers"]![0]!.AsObject();
+ JsonObject producer_Current = current["producers"]![0]!.AsObject();
+ Assert.Equal(producer_Historical.Select(x => x.Key).OrderBy(x => x), producer_Current.Select(x => x.Key).OrderBy(x => x));
+ }
+
+ [Fact]
+ public void OpenStudio_NewManifest_KeepsEveryPreS03Key()
+ {
+ OpenStudioSimulationRun run = Model(null).RunOpenStudioSimulation(directory, null, null);
+ JsonObject manifest = Manifest(run.ManifestPath);
+
+ string[] preS03 = { "schema", "id", "status", "startedUtc", "finishedUtc", "durationSeconds", "sourcePath", "model", "snapshot", "weather", "engine", "outputs", "resultCount", "removedResultCount", "diagnostics", "resultDiagnostics", "error" };
+ Assert.Equal(preS03.Append("provenance").OrderBy(x => x), manifest.Select(x => x.Key).OrderBy(x => x));
+ Assert.Equal(OpenStudioSimulationRun.Schema, (string)manifest["schema"]!);
+ }
+
+ // ---------------------------------------------------------------- helpers
+
+ /// Every "unknown" in the provenance has a non-empty reason, and no item outside the copied settings is null or blank.
+ private static void AssertEveryUnknownHasAReason(JsonObject provenance)
+ {
+ JsonObject unknown = provenance["unknown"]!.AsObject();
+ Assert.All(unknown, x => Assert.False(string.IsNullOrWhiteSpace((string?)x.Value)));
+
+ int count = 0;
+ Walk(provenance, string.Empty);
+ Assert.True(unknown.Count >= count, string.Format("{0} unknown items but {1} reasons", count, unknown.Count));
+
+ //...and the other way round: every reason names an item that is "unknown" (none left behind by a section that became unknown).
+ foreach (string itemPath in unknown.Select(x => x.Key).Where(x => !x.Contains('[')))
+ {
+ JsonNode? jsonNode = provenance;
+ foreach (string name in itemPath.Split('.'))
+ {
+ jsonNode = jsonNode is JsonObject jsonObject ? jsonObject[name] : null;
+ }
+
+ Assert.True(jsonNode is JsonValue jsonValue && jsonValue.TryGetValue(out string? text) && text == SimulationRunProvenance.Unknown, "the reason for '" + itemPath + "' names no unknown item");
+ }
+
+ void Walk(JsonNode? jsonNode, string path)
+ {
+ switch (jsonNode)
+ {
+ case null:
+ Assert.Fail("a null item at '" + path + "'");
+ break;
+ case JsonObject jsonObject:
+ foreach (KeyValuePair keyValuePair in jsonObject)
+ {
+ //The copied engine settings keep their own nulls ("not set"); the reasons map is checked above.
+ if ((path.Length == 0 && (keyValuePair.Key == "settings" || keyValuePair.Key == "unknown")))
+ {
+ continue;
+ }
+
+ Walk(keyValuePair.Value, path.Length == 0 ? keyValuePair.Key : path + "." + keyValuePair.Key);
+ }
+
+ break;
+ case JsonArray jsonArray:
+ foreach (JsonNode? item in jsonArray)
+ {
+ Walk(item, path + "[]");
+ }
+
+ break;
+ case JsonValue jsonValue when jsonValue.TryGetValue(out string? text):
+ Assert.False(string.IsNullOrWhiteSpace(text), "a blank item at '" + path + "'");
+ if (text == SimulationRunProvenance.Unknown)
+ {
+ count++;
+ if (!path.Contains("[]"))
+ {
+ Assert.True(unknown.ContainsKey(path), "no reason for the unknown item '" + path + "'");
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
+ private static JsonObject Manifest(string? path)
+ {
+ Assert.True(path != null && File.Exists(path), "no manifest at " + path);
+ return JsonNode.Parse(File.ReadAllText(path!))!.AsObject();
+ }
+
+ private TasSimulationRun TasRun(TasSimulationStatus status)
+ {
+ return new TasSimulationRun
+ {
+ Id = "0123abcd",
+ Status = status,
+ StartedUtc = DateTime.UtcNow,
+ Directory = directory,
+ ProjectName = "Box",
+ ModelName = "Box",
+ ModelGuid = Guid.NewGuid(),
+ TbdPath = Path.Combine(directory, "Box.tbd"),
+ };
+ }
+
+ /// As Modify.RunPartOSimulation builds them: design days derived from the run's weather.
+ private static WorkflowSettings WorkflowSettingsAsRunPartOSimulationBuildsThem(WeatherData weatherData, int from, int to)
+ {
+ return new WorkflowSettings
+ {
+ Path_TBD = @"C:\runs\Box.tbd",
+ WeatherData = weatherData,
+ DesignDays_Heating = new List { Analytical.Query.HeatingDesignDay(weatherData) },
+ DesignDays_Cooling = new List { Analytical.Query.CoolingDesignDay(weatherData) },
+ Sizing = true,
+ UnmetHours = true,
+ Simulate = true,
+ SimulateFrom = from,
+ SimulateTo = to,
+ };
+ }
+
+ private static JsonObject Inputs()
+ {
+ return Query.TasRunInputs(true, true, 1, 365, true, true, false, SolarCalculationMethod.TAS, false, false, false, false, false, false, false, null);
+ }
+
+ private static WeatherData Weather(string name, double offset)
+ {
+ WeatherYear weatherYear = new WeatherYear(2018);
+ for (int day = 0; day < 365; day++)
+ {
+ for (int hour = 0; hour < 24; hour++)
+ {
+ double temperature = offset + 10.0 * Math.Sin(2 * Math.PI * day / 365.0) + 3.0 * Math.Sin(2 * Math.PI * hour / 24.0);
+ weatherYear.Add(day, hour, new Dictionary
+ {
+ { WeatherDataType.DryBulbTemperature.ToString(), temperature },
+ { WeatherDataType.RelativeHumidity.ToString(), 60.0 },
+ { WeatherDataType.GlobalSolarRadiation.ToString(), hour > 6 && hour < 18 ? 300.0 : 0.0 },
+ });
+ }
+ }
+
+ return new WeatherData(name, name, 51.5, -0.1, 0, weatherYear);
+ }
+
+ private static AnalyticalModel Model(WeatherData? weatherData)
+ {
+ AnalyticalModel analyticalModel = new AnalyticalModel(Guid.NewGuid(), "Box");
+ Assert.True(analyticalModel.AddSpace(new Space("Room 1", new Geometry.Spatial.Point3D(1, 1, 1))));
+ if (weatherData != null)
+ {
+ analyticalModel.SetValue(Analytical.AnalyticalModelParameter.WeatherData, weatherData);
+ }
+
+ return analyticalModel;
+ }
+
+ private static AnalyticalModel EngineModel()
+ {
+ return Model(null);
+ }
+
+ private string Epw()
+ {
+ string path = Path.Combine(directory, "test.epw");
+ if (!File.Exists(path))
+ {
+ File.WriteAllText(path, "LOCATION,Test,,,TMY,000000,0,0,0,0\n");
+ }
+
+ return path;
+ }
+
+ private string ProducerPath(string fileName, string content = "")
+ {
+ string path = Path.Combine(directory, "producers", fileName);
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ File.WriteAllText(path, content);
+ return path;
+ }
+
+ private static string Argument(EngineComparisonProducerRun producerRun, string name)
+ {
+ int index = producerRun.Arguments.IndexOf(name);
+ return index < 0 ? null! : producerRun.Arguments[index + 1];
+ }
+
+ private static string Resource(string name, [CallerFilePath] string? sourceFilePath = null)
+ {
+ return Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "Resources", "EngineComparison", name);
+ }
+
+ private static string Evidence(string a, string b, string c, string d, [CallerFilePath] string? sourceFilePath = null)
+ {
+ return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(sourceFilePath)!, "..", "..", "documentation", "evidence", a, b, c, d));
+ }
+
+ private static string TasDocument => Resource("scenario-benchmark-TAS.json");
+
+ private static string OpenStudioDocument => Resource("scenario-benchmark-OpenStudio.json");
+ }
+}
diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/TasRunManifestTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/TasRunManifestTests.cs
new file mode 100644
index 00000000..11a4c79b
--- /dev/null
+++ b/WPF/SAM.Analytical.UI.WPF.Tests/TasRunManifestTests.cs
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
+
+#nullable enable
+
+using SAM.Analytical.Tas;
+using SAM.Core;
+using SAM.Weather;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json.Nodes;
+using System.Threading;
+using Xunit;
+
+namespace SAM.Analytical.UI.WPF.Tests
+{
+ ///
+ /// S03, Tas only: the ordinary Energy Simulation / Run Simulation > Tas core writes <project>.tas-run.json beside the TBD for
+ /// every outcome - success, failure, cancel, a throw and a refusal - with the workflow settings that actually ran. The TAS workflow step
+ /// is faked through the core's existing seam, so no TAS is needed; the core's progress window makes these STA tests.
+ ///
+ [Collection(WpfCollection.Name)]
+ public class TasRunManifestTests : IDisposable
+ {
+ private readonly string directory;
+
+ public TasRunManifestTests()
+ {
+ directory = Path.Combine(Path.GetTempPath(), "SAM-UI-S03-tas-tests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(directory);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ Directory.Delete(directory, true);
+ }
+ catch (IOException)
+ {
+ }
+ catch (UnauthorizedAccessException)
+ {
+ }
+ }
+
+ [WpfFact]
+ public void Succeeded_RecordsTheWorkflowSettingsThatRan_AndTheEmbeddedWeather()
+ {
+ WeatherData weatherData = Weather("Embedded", 0.0);
+ UIAnalyticalModel uIAnalyticalModel = Open(weatherData);
+ WorkflowSettings? workflowSettings_Seen = null;
+
+ PartOSimulationOutcome partOSimulationOutcome = Modify.Simulate(uIAnalyticalModel, "Box", directory, new WeatherData(weatherData), null, (AnalyticalModel analyticalModel, WorkflowSettings workflowSettings, CancellationToken cancellationToken, out bool cancelled) =>
+ {
+ cancelled = false;
+ workflowSettings_Seen = workflowSettings;
+ File.WriteAllText(workflowSettings.Path_TBD, "tbd");
+ File.WriteAllText(Path.ChangeExtension(workflowSettings.Path_TBD, "tsd"), "tsd");
+ return analyticalModel;
+ });
+
+ Assert.True(partOSimulationOutcome.Ran);
+ JsonObject manifest = Manifest();
+ Assert.Equal(TasSimulationRun.Schema, (string)manifest["schema"]!);
+ Assert.Equal("Succeeded", (string)manifest["status"]!);
+ Assert.Equal(Path.Combine(directory, "Box.tbd"), (string)manifest["outputs"]!["tbd"]!);
+ Assert.Equal(Path.Combine(directory, "Box.tsd"), (string)manifest["outputs"]!["tsd"]!);
+ Assert.Equal("Box", (string)manifest["projectName"]!);
+
+ //The settings the workflow was handed, as it was handed them.
+ JsonObject provenance = manifest["provenance"]!.AsObject();
+ JsonObject workflow = provenance["settings"]!["workflow"]!.AsObject();
+ Assert.NotNull(workflowSettings_Seen);
+ Assert.Equal(workflowSettings_Seen!.SimulateFrom, (int)workflow["SimulateFrom"]!);
+ Assert.Equal(workflowSettings_Seen.SimulateTo, (int)workflow["SimulateTo"]!);
+ Assert.Equal(365, (int)workflow["SimulateTo"]!);
+ Assert.Equal(workflowSettings_Seen.Sizing, (bool)workflow["Sizing"]!);
+ Assert.Equal(workflowSettings_Seen.UnmetHours, (bool)workflow["UnmetHours"]!);
+ Assert.Equal("Direct", (string)workflow["T3DRoute"]!);
+ Assert.Equal("Embedded", (string)provenance["weather"]!["source"]!);
+ Assert.Equal("DerivedFromWeather", (string)provenance["designDays"]!["source"]!);
+ Assert.Equal("Tas", (string)provenance["route"]!);
+ }
+
+ [WpfFact]
+ public void ExplicitWeatherFromAFile_IsRecordedWithTheFilesHash()
+ {
+ UIAnalyticalModel uIAnalyticalModel = Open(Weather("Embedded", 0.0));
+ string path_Weather = Path.Combine(directory, "chosen.epw");
+ File.WriteAllText(path_Weather, "LOCATION,Chosen,,,TMY,000000,0,0,0,0\n");
+
+ Modify.Simulate(uIAnalyticalModel, "Box", directory, Weather("Chosen", 4.0), path_Weather, (AnalyticalModel analyticalModel, WorkflowSettings workflowSettings, CancellationToken cancellationToken, out bool cancelled) =>
+ {
+ cancelled = false;
+ return analyticalModel;
+ });
+
+ JsonObject weather = Manifest()["provenance"]!["weather"]!.AsObject();
+ Assert.Equal("Explicit", (string)weather["source"]!);
+ Assert.Equal(Query.Sha256(path_Weather), (string)weather["file"]!["sha256"]!);
+ }
+
+ /// S01b: a workflow that returns no model is failed - decided by the workflow, whatever files are in the folder.
+ [WpfFact]
+ public void Failed_IsRecordedAsFailed_WhateverFilesAreInTheFolder()
+ {
+ UIAnalyticalModel uIAnalyticalModel = Open(Weather("Embedded", 0.0));
+
+ PartOSimulationOutcome partOSimulationOutcome = Modify.Simulate(uIAnalyticalModel, "Box", directory, Weather("Embedded", 0.0), null, (AnalyticalModel analyticalModel, WorkflowSettings workflowSettings, CancellationToken cancellationToken, out bool cancelled) =>
+ {
+ cancelled = false;
+ File.WriteAllBytes(Path.ChangeExtension(workflowSettings.Path_TBD, "tsd"), new byte[1024]);
+ return null;
+ });
+
+ Assert.NotNull(partOSimulationOutcome.Failure);
+ JsonObject manifest = Manifest();
+ Assert.Equal("Failed", (string)manifest["status"]!);
+ Assert.True(manifest["provenance"]!["settings"]!["workflow"] is JsonObject);
+ }
+
+ [WpfFact]
+ public void Cancelled_IsRecordedAsCancelled()
+ {
+ UIAnalyticalModel uIAnalyticalModel = Open(Weather("Embedded", 0.0));
+
+ PartOSimulationOutcome partOSimulationOutcome = Modify.Simulate(uIAnalyticalModel, "Box", directory, Weather("Embedded", 0.0), null, (AnalyticalModel analyticalModel, WorkflowSettings workflowSettings, CancellationToken cancellationToken, out bool cancelled) =>
+ {
+ cancelled = true;
+ return null;
+ });
+
+ Assert.True(partOSimulationOutcome.Cancelled);
+ Assert.Equal("Cancelled", (string)Manifest()["status"]!);
+ }
+
+ /// A throw from the workflow still propagates exactly as before; the record says Failed and carries the error.
+ [WpfFact]
+ public void Exception_StillPropagates_AndIsRecordedAsFailed()
+ {
+ UIAnalyticalModel uIAnalyticalModel = Open(Weather("Embedded", 0.0));
+
+ InvalidOperationException exception = Assert.Throws(() => Modify.Simulate(uIAnalyticalModel, "Box", directory, Weather("Embedded", 0.0), null, (AnalyticalModel analyticalModel, WorkflowSettings workflowSettings, CancellationToken cancellationToken, out bool cancelled) =>
+ {
+ throw new InvalidOperationException("TBD server died");
+ }));
+
+ Assert.Equal("TBD server died", exception.Message);
+ JsonObject manifest = Manifest();
+ Assert.Equal("Failed", (string)manifest["status"]!);
+ Assert.Contains("TBD server died", (string)manifest["error"]!);
+ }
+
+ /// A run that could not start (the old TBD cannot be replaced) is Refused, says why, and has no workflow settings.
+ [WpfFact]
+ public void Refused_IsRecordedAsRefused_WithTheReason()
+ {
+ UIAnalyticalModel uIAnalyticalModel = Open(Weather("Embedded", 0.0));
+ string path_TBD = Path.Combine(directory, "Box.tbd");
+ File.WriteAllText(path_TBD, "locked");
+ bool ran = false;
+
+ using (new FileStream(path_TBD, FileMode.Open, FileAccess.Read, FileShare.None))
+ {
+ Modify.Simulate(uIAnalyticalModel, "Box", directory, Weather("Embedded", 0.0), null, (AnalyticalModel analyticalModel, WorkflowSettings workflowSettings, CancellationToken cancellationToken, out bool cancelled) =>
+ {
+ ran = true;
+ cancelled = false;
+ return analyticalModel;
+ });
+ }
+
+ Assert.False(ran);
+ JsonObject manifest = Manifest();
+ Assert.Equal("Refused", (string)manifest["status"]!);
+ Assert.Contains("could not be overwritten", (string)manifest["refusal"]!);
+ Assert.Equal("unknown", (string)manifest["provenance"]!["settings"]!["workflow"]!);
+ }
+
+ private JsonObject Manifest()
+ {
+ string path = TasSimulationRun.ManifestPathOf(directory, "Box");
+ Assert.True(File.Exists(path), "no Tas run record at " + path);
+ return JsonNode.Parse(File.ReadAllText(path))!.AsObject();
+ }
+
+ private UIAnalyticalModel Open(WeatherData weatherData)
+ {
+ AnalyticalModel analyticalModel = new AnalyticalModel(Guid.NewGuid(), "Box");
+ Assert.True(analyticalModel.AddSpace(new Space("Room 1", new Geometry.Spatial.Point3D(1, 1, 1))));
+ analyticalModel.SetValue(Analytical.AnalyticalModelParameter.WeatherData, weatherData);
+
+ string path = Path.Combine(directory, "Box.json");
+ Assert.True(Core.Convert.ToFile(new IJSAMObject[] { analyticalModel }, path));
+ UIAnalyticalModel uIAnalyticalModel = new UIAnalyticalModel(path);
+ Assert.True(uIAnalyticalModel.Open());
+ return uIAnalyticalModel;
+ }
+
+ private static WeatherData Weather(string name, double offset)
+ {
+ WeatherYear weatherYear = new WeatherYear(2018);
+ for (int day = 0; day < 365; day++)
+ {
+ for (int hour = 0; hour < 24; hour++)
+ {
+ double temperature = offset + 10.0 * Math.Sin(2 * Math.PI * day / 365.0) + 3.0 * Math.Sin(2 * Math.PI * hour / 24.0);
+ weatherYear.Add(day, hour, new Dictionary
+ {
+ { WeatherDataType.DryBulbTemperature.ToString(), temperature },
+ { WeatherDataType.RelativeHumidity.ToString(), 60.0 },
+ });
+ }
+ }
+
+ return new WeatherData(name, name, 51.5, -0.1, 0, weatherYear);
+ }
+ }
+}
diff --git a/WPF/SAM.Analytical.UI.WPF/Classes/Comparison/EngineComparisonRun.cs b/WPF/SAM.Analytical.UI.WPF/Classes/Comparison/EngineComparisonRun.cs
index 4fff5107..3f417ab8 100644
--- a/WPF/SAM.Analytical.UI.WPF/Classes/Comparison/EngineComparisonRun.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Classes/Comparison/EngineComparisonRun.cs
@@ -102,6 +102,12 @@ public sealed class EngineComparisonRun
public string? WeatherSha256 { get; set; }
+ /// The EPW the user chose, as given; null for the embedded weather or none.
+ public string? WeatherOriginalPath { get; set; }
+
+ /// The run's provenance (S03): this build, each producer's identity and what each producer reported.
+ public SimulationRunProvenance? Provenance { get; set; }
+
public List ProducerRuns { get; } = new List();
/// SAM_Validation's overall gate ("Pass", "Warn", "Fail") when the documents were compared.
@@ -161,6 +167,7 @@ public string ToJson()
["reports"] = reportPaths,
["diagnostics"] = diagnostics,
["error"] = Exception == null ? null : Exception.GetType().Name + ": " + Exception.Message,
+ ["provenance"] = Provenance?.ToJsonObject(),
};
return jsonObject.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
diff --git a/WPF/SAM.Analytical.UI.WPF/Classes/OpenStudio/OpenStudioSimulationRun.cs b/WPF/SAM.Analytical.UI.WPF/Classes/OpenStudio/OpenStudioSimulationRun.cs
index d4750599..9bcef383 100644
--- a/WPF/SAM.Analytical.UI.WPF/Classes/OpenStudio/OpenStudioSimulationRun.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Classes/OpenStudio/OpenStudioSimulationRun.cs
@@ -75,6 +75,24 @@ public sealed class OpenStudioSimulationRun
public string? WeatherSha256 { get; set; }
+ /// The EPW the user chose, as given; null for the embedded weather or none.
+ public string? WeatherOriginalPath { get; set; }
+
+ /// The loaded OpenStudio SDK's version; null when the SDK was not loaded.
+ public string? OpenStudioSdkVersion { get; set; }
+
+ /// The EnergyPlus version the loaded SDK targets (the version that ran is read from eplusout.err).
+ public string? EnergyPlusSdkVersion { get; set; }
+
+ /// The design-day basis SAM_OpenStudio's conversion established ("None", "Ddy", "EmbeddedModel"); null without a conversion result.
+ public string? DesignDaySource { get; set; }
+
+ /// The OpenStudioRunOptions and conversion arguments the run was given, as values.
+ public JsonObject? RunOptions { get; set; }
+
+ /// The run's provenance (S03), completed when the run ends.
+ public SimulationRunProvenance? Provenance { get; set; }
+
/// The SAM.Analytical.OpenStudio build that converted the model (its InformationalVersion).
public string? SamOpenStudioVersion { get; set; }
@@ -167,6 +185,7 @@ public JsonObject ToJsonObject()
["diagnostics"] = ToJsonArray(Diagnostics),
["resultDiagnostics"] = ToJsonArray(ResultDiagnostics),
["error"] = Exception == null ? null : Exception.GetType().Name + ": " + Exception.Message,
+ ["provenance"] = Provenance?.ToJsonObject(),
};
return result;
diff --git a/WPF/SAM.Analytical.UI.WPF/Classes/Provenance/SimulationRunProvenance.cs b/WPF/SAM.Analytical.UI.WPF/Classes/Provenance/SimulationRunProvenance.cs
new file mode 100644
index 00000000..01844b66
--- /dev/null
+++ b/WPF/SAM.Analytical.UI.WPF/Classes/Provenance/SimulationRunProvenance.cs
@@ -0,0 +1,513 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+
+namespace SAM.Analytical.UI.WPF
+{
+ ///
+ /// The run provenance (roadmap S03) that every Tas, OpenStudio and Both run manifest carries as "provenance": which build ran,
+ /// which engine and producer binaries, which weather and design days, and the settings actually handed to the engine.
+ ///
+ /// Nothing is assumed or filled in. Every item is read from the running binaries, the files of this run or the settings object the
+ /// engine was given. An item that cannot be established is written as , and its reason is recorded under
+ /// "unknown" by item path - an item is never blank. The source revision is the build's own stamp (the + build metadata of
+ /// its InformationalVersion, which SAM_Deploy installers set); a local Git HEAD is never read, because it need not be what was built.
+ ///
+ ///
+ /// Collecting it must never change how a run ends: each collector catches its own failures and records them as unknown.
+ ///
+ ///
+ public sealed class SimulationRunProvenance
+ {
+ /// Schema identifier of the provenance block, versioned independently of the manifests that carry it.
+ public const string Schema = "sam-ui.run-provenance/1";
+
+ /// The value written for an item that could not be established; its reason is under "unknown".
+ public const string Unknown = "unknown";
+
+ private static readonly Regex SourceRevisionPattern = new Regex("^[0-9a-f]{7,64}$", RegexOptions.CultureInvariant);
+
+ private static readonly Regex EnergyPlusVersionPattern = new Regex(@"EnergyPlus,\s*Version\s+([^,]+)", RegexOptions.CultureInvariant);
+
+ private static readonly Dictionary<(string, long, DateTime), string> sha256Cache = new Dictionary<(string, long, DateTime), string>();
+
+ private readonly JsonObject unknown = new JsonObject();
+
+ private readonly JsonArray components = new JsonArray();
+
+ private readonly HashSet componentNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ /// The workflow: "Tas", "OpenStudio" or "Both".
+ public SimulationRunProvenance(string route)
+ {
+ Root = new ProvenanceSection(this, new JsonObject(), null);
+ Root.Json["schema"] = Schema;
+ Root.Json["route"] = route;
+
+ ProvenanceSection application = Root.Section("application");
+ Guard(application, "entryAssembly", () => SetAssembly(application.Section("entryAssembly"), Assembly.GetEntryAssembly()));
+
+ //The SAM_UI assembly's own stamp: the content identity of an installed build (SAM_Deploy sets it on every SAM assembly).
+ Guard(application, "sourceRevision", () =>
+ {
+ string? informationalVersion = InformationalVersion(typeof(SimulationRunProvenance).Assembly);
+ string? sourceRevision = SourceRevision(informationalVersion);
+ application.Set("sourceRevision", sourceRevision, informationalVersion == null
+ ? "this build carries no InformationalVersion, so no source revision was stamped into it (a local developer build); the component SHA-256 values identify the binaries"
+ : "this build's InformationalVersion '" + informationalVersion + "' has no source revision after '+' (a local developer build); the component SHA-256 values identify the binaries");
+ });
+ application.Json["sourceRevisionOf"] = "the +build metadata of SAM.Analytical.UI.WPF's InformationalVersion; SAM_Deploy installers stamp their SAM_Deploy commit, which pins every repository";
+ application.Json["components"] = components;
+
+ AddComponent(typeof(Core.SAMObject).Assembly);
+ AddComponent(typeof(AnalyticalModel).Assembly);
+ AddComponent(typeof(SimulationRunProvenance).Assembly);
+ }
+
+ /// The provenance object itself; sections are added to it by the run that owns it.
+ public ProvenanceSection Root { get; }
+
+ /// Records the identity of one loaded SAM assembly the run used (once per assembly).
+ public void AddComponent(Assembly? assembly)
+ {
+ string name = assembly?.GetName().Name ?? "(none)";
+ if (assembly == null || !componentNames.Add(name))
+ {
+ return;
+ }
+
+ ProvenanceSection section = new ProvenanceSection(this, new JsonObject(), string.Format("application.components[{0}]", name));
+ Guard(section, "assembly", () => SetAssembly(section, assembly));
+ components.Add(section.Json);
+ }
+
+ /// The provenance as written to a manifest; a copy, so the run can keep adding to it.
+ public JsonObject ToJsonObject()
+ {
+ JsonObject result = (JsonObject)Root.Json.DeepClone();
+ result["unknown"] = unknown.DeepClone();
+ return result;
+ }
+
+ internal void ForgetUnknownUnder(string prefix)
+ {
+ foreach (string itemPath in unknown.Select(x => x.Key).Where(x => x.StartsWith(prefix, StringComparison.Ordinal)).ToList())
+ {
+ unknown.Remove(itemPath);
+ }
+ }
+
+ internal void RecordUnknown(string itemPath, string reason)
+ {
+ unknown[itemPath] = string.IsNullOrWhiteSpace(reason) ? "no reason was given" : reason;
+ }
+
+ /// Runs one collector; whatever it throws is recorded as the reason is unknown, never passed on.
+ public static void Guard(ProvenanceSection section, string key, Action action)
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception exception)
+ {
+ section.SetUnknown(key, "it could not be read: " + exception.GetType().Name + ": " + exception.Message);
+ }
+ }
+
+ /// Name, version, InformationalVersion, file and SHA-256 of a loaded assembly.
+ public static void SetAssembly(ProvenanceSection section, Assembly? assembly)
+ {
+ if (assembly == null)
+ {
+ section.SetUnknown("name", "the process has no managed entry assembly (hosted)");
+ return;
+ }
+
+ AssemblyName assemblyName = assembly.GetName();
+ section.Set("name", assemblyName.Name, "the assembly has no name");
+ section.Set("version", assemblyName.Version?.ToString(), "the assembly has no version");
+ section.Set("informationalVersion", InformationalVersion(assembly), "the assembly carries no InformationalVersion");
+
+ string location = assembly.IsDynamic ? string.Empty : assembly.Location;
+ if (string.IsNullOrWhiteSpace(location))
+ {
+ section.SetUnknown("path", "the assembly was not loaded from a file");
+ section.SetUnknown("sha256", "the assembly was not loaded from a file");
+ return;
+ }
+
+ section.Json["path"] = location;
+ section.Set("sha256", Sha256(location), "the assembly file could not be read");
+ }
+
+ ///
+ /// Path, SHA-256, file version and product version of an executable. For a .NET apphost (an .exe beside a .dll of the same name), the
+ /// .dll is the code that runs, so it is described as well, as "assembly".
+ ///
+ public static void SetExecutable(ProvenanceSection section, string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ section.SetUnknown("path", "no executable was located");
+ return;
+ }
+
+ section.Json["path"] = path;
+ if (!File.Exists(path))
+ {
+ section.SetUnknown("sha256", "the executable does not exist at this path");
+ return;
+ }
+
+ SetFile(section, path!);
+
+ string path_Assembly = Path.ChangeExtension(path!, ".dll");
+ if (string.Equals(Path.GetExtension(path), ".exe", StringComparison.OrdinalIgnoreCase) && File.Exists(path_Assembly))
+ {
+ ProvenanceSection assembly = section.Section("assembly");
+ assembly.Json["path"] = path_Assembly;
+ SetFile(assembly, path_Assembly);
+ }
+ }
+
+ private static void SetFile(ProvenanceSection section, string path)
+ {
+ Guard(section, "sha256", () => section.Set("sha256", Sha256(path), "the file could not be read"));
+ Guard(section, "fileVersion", () =>
+ {
+ FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(path);
+ section.Set("fileVersion", Blank(fileVersionInfo.FileVersion), "the file has no version resource");
+ section.Set("productVersion", Blank(fileVersionInfo.ProductVersion), "the file has no version resource");
+ });
+ }
+
+ ///
+ /// The Tas engine the in-process Tas route drives: the COM server registered for TBD.Document (what COM starts for the
+ /// workflow), its product version and SHA-256.
+ ///
+ /// The server's registered command line (); null when none is registered.
+ public static void SetTasEngine(ProvenanceSection section, string? comServerCommand)
+ {
+ section.Json["comServer"] = "TBD.Document";
+ string? path = CommandPath(comServerCommand);
+ if (path == null)
+ {
+ section.SetUnknown("version", "no TBD.Document COM server is registered on this machine, so the Tas version and executable could not be read");
+ section.SetUnknown("executable", "no TBD.Document COM server is registered on this machine");
+ return;
+ }
+
+ ProvenanceSection executable = section.Section("executable");
+ SetExecutable(executable, path);
+
+ string? version = executable.Json["productVersion"] is JsonValue jsonValue && jsonValue.TryGetValue(out string? productVersion) && productVersion != Unknown ? productVersion : null;
+ section.Set("version", version, "the registered Tas executable has no product version (" + path + ")");
+ }
+
+ /// The registered command line of the TBD.Document COM server (LocalServer32), or null. Read only; never throws.
+ public static string? TasComServerCommand()
+ {
+ //Both registry views: a 32-bit-only registration is not visible in the 64-bit view, and the reverse.
+ foreach (Microsoft.Win32.RegistryView registryView in new[] { Microsoft.Win32.RegistryView.Registry64, Microsoft.Win32.RegistryView.Registry32 })
+ {
+ try
+ {
+ using Microsoft.Win32.RegistryKey classesRoot = Microsoft.Win32.RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.ClassesRoot, registryView);
+ using Microsoft.Win32.RegistryKey? progId = classesRoot.OpenSubKey(@"TBD.Document\CLSID");
+ string? clsid = progId?.GetValue(null) as string;
+ if (string.IsNullOrWhiteSpace(clsid))
+ {
+ continue;
+ }
+
+ using Microsoft.Win32.RegistryKey? localServer = classesRoot.OpenSubKey(@"CLSID\" + clsid + @"\LocalServer32");
+ if (localServer?.GetValue(null) is string command && !string.IsNullOrWhiteSpace(command))
+ {
+ return command;
+ }
+ }
+ catch (Exception)
+ {
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// "sha256:" and the SHA-256 of a WeatherData's canonical JSON v1 without its own top-level Guid: the same weather read twice (each
+ /// read gives the object a new Guid) has the same content hash; any change to its data changes it.
+ ///
+ public static string WeatherContentSha256(Weather.WeatherData weatherData)
+ {
+ JsonObject jsonObject = weatherData.ToJsonObject() ?? throw new InvalidOperationException("the weather has no JSON form");
+ jsonObject.Remove("Guid");
+ return Benchmark.BenchmarkCanonicalJson.ComputeSha256(jsonObject.ToJsonString());
+ }
+
+ /// The executable of a registered command line: the quoted part, else up to the first ".exe"; 8.3 names are expanded. Null when none.
+ public static string? CommandPath(string? command)
+ {
+ if (string.IsNullOrWhiteSpace(command))
+ {
+ return null;
+ }
+
+ command = command!.Trim();
+ string path;
+ if (command.StartsWith("\"", StringComparison.Ordinal))
+ {
+ int end = command.IndexOf('"', 1);
+ path = end > 1 ? command.Substring(1, end - 1) : command.Trim('"');
+ }
+ else
+ {
+ int index = command.IndexOf(".exe", StringComparison.OrdinalIgnoreCase);
+ path = index >= 0 ? command.Substring(0, index + 4) : command;
+ }
+
+ try
+ {
+ return File.Exists(path) ? new FileInfo(path).FullName : path;
+ }
+ catch (Exception)
+ {
+ return path;
+ }
+ }
+
+ /// The EnergyPlus version that actually ran, from the first line of its eplusout.err ("Program Version,EnergyPlus, Version x"); null when not there.
+ public static string? EnergyPlusVersionFromErr(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
+ {
+ return null;
+ }
+
+ using StreamReader streamReader = new StreamReader(path!);
+ string? line = streamReader.ReadLine();
+ Match match = line == null ? Match.Empty : EnergyPlusVersionPattern.Match(line);
+ return match.Success ? match.Groups[1].Value.Trim() : null;
+ }
+
+ /// The 7-64 lowercase hex source revision after '+' in an InformationalVersion, or null when there is none.
+ public static string? SourceRevision(string? informationalVersion)
+ {
+ int plus = informationalVersion?.IndexOf('+') ?? -1;
+ if (plus < 0)
+ {
+ return null;
+ }
+
+ string candidate = informationalVersion!.Substring(plus + 1).Trim().ToLowerInvariant();
+ return SourceRevisionPattern.IsMatch(candidate) ? candidate : null;
+ }
+
+ /// "sha256:" and the SHA-256 of a SAM object's canonical JSON v1 (SAM_Validation's helper): a content identity for data that is not a file.
+ public static string CanonicalSha256(Core.IJSAMObject jSAMObject)
+ {
+ JsonObject jsonObject = jSAMObject.ToJsonObject() ?? throw new InvalidOperationException(jSAMObject.GetType().Name + " has no JSON form");
+ return Benchmark.BenchmarkCanonicalJson.ComputeSha256(jsonObject.ToJsonString());
+ }
+
+ ///
+ /// The provenance a producer reported in its B1a document (samCommit, runnerCommit, engine, route, weather, design days, model hashes,
+ /// state), copied as written. Never throws: an unreadable document or one whose provenance is not an object is recorded as unknown.
+ ///
+ public static void SetReportedProvenance(ProvenanceSection section, string key, string? benchmarkPath)
+ {
+ if (string.IsNullOrWhiteSpace(benchmarkPath) || !File.Exists(benchmarkPath))
+ {
+ section.SetUnknown(key, "the producer wrote no B1a document");
+ return;
+ }
+
+ JsonNode? provenance;
+ try
+ {
+ provenance = JsonNode.Parse(File.ReadAllText(benchmarkPath!))?["provenance"];
+ }
+ catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException || exception is System.Text.Json.JsonException || exception is InvalidOperationException)
+ {
+ section.SetUnknown(key, "the producer's B1a document could not be read: " + exception.GetType().Name + ": " + exception.Message);
+ return;
+ }
+
+ if (provenance is not JsonObject jsonObject)
+ {
+ section.SetUnknown(key, "the producer's B1a document has no provenance object");
+ return;
+ }
+
+ ProvenanceSection reported = section.Section(key);
+ foreach (string name in new[] { "samCommit", "runnerCommit", "engine", "route", "weather", "designDaySource", "sourceFileHash", "canonicalModelHash", "canonicalizationVersion", "state" })
+ {
+ JsonNode? jsonNode = jsonObject[name];
+ if (jsonNode == null)
+ {
+ reported.SetUnknown(name, "the producer did not report it (absent or null in its B1a document; its warnings and notes say why)");
+ }
+ else
+ {
+ reported.Json[name] = jsonNode.DeepClone();
+ }
+ }
+
+ //A null inside a reported object (an engine version the producer could not read, say) is the producer's own "not known".
+ ReplaceNulls(reported, reported.Json, string.Empty, "the producer reported none (null in its B1a document; its warnings and notes say why)");
+ }
+
+ private static void ReplaceNulls(ProvenanceSection section, JsonObject jsonObject, string prefix, string reason)
+ {
+ foreach (string name in jsonObject.Select(x => x.Key).ToList())
+ {
+ string key = prefix.Length == 0 ? name : prefix + "." + name;
+ if (jsonObject[name] == null)
+ {
+ jsonObject[name] = Unknown;
+ section.RecordUnknown(key, reason);
+ }
+ else if (jsonObject[name] is JsonObject child)
+ {
+ ReplaceNulls(section, child, key, reason);
+ }
+ }
+ }
+
+ /// The environment variables named, as this process passes them to a child: their value, or "not set".
+ public static void SetEnvironment(ProvenanceSection section, params string[] names)
+ {
+ foreach (string name in names)
+ {
+ string? value = Environment.GetEnvironmentVariable(name);
+ section.Json[name] = string.IsNullOrEmpty(value) ? "not set" : value;
+ }
+ }
+
+ public static string? InformationalVersion(Assembly assembly)
+ {
+ return Blank(assembly.GetCustomAttribute()?.InformationalVersion);
+ }
+
+ /// "sha256:" and the SHA-256 of a file's bytes (the B1a notation); cached per path, length and write time.
+ public static string? Sha256(string path)
+ {
+ FileInfo fileInfo = new FileInfo(path);
+ if (!fileInfo.Exists)
+ {
+ return null;
+ }
+
+ (string, long, DateTime) key = (fileInfo.FullName.ToLowerInvariant(), fileInfo.Length, fileInfo.LastWriteTimeUtc);
+ lock (sha256Cache)
+ {
+ if (sha256Cache.TryGetValue(key, out string? cached))
+ {
+ return cached;
+ }
+ }
+
+ string result;
+ using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
+ {
+ result = "sha256:" + System.Convert.ToHexString(SHA256.HashData(fileStream)).ToLowerInvariant();
+ }
+
+ lock (sha256Cache)
+ {
+ sha256Cache[key] = result;
+ }
+
+ return result;
+ }
+
+ private static string? Blank(string? value)
+ {
+ return string.IsNullOrWhiteSpace(value) ? null : value!.Trim();
+ }
+ }
+
+ /// One object inside a , which knows its item path so an unknown item's reason is filed under it.
+ public sealed class ProvenanceSection
+ {
+ private readonly SimulationRunProvenance owner;
+
+ internal ProvenanceSection(SimulationRunProvenance owner, JsonObject json, string? path)
+ {
+ this.owner = owner;
+ Json = json;
+ Path = path;
+ }
+
+ public JsonObject Json { get; }
+
+ /// The dotted path of this object inside the provenance; null for the provenance itself.
+ public string? Path { get; }
+
+ /// The child object , created when absent.
+ public ProvenanceSection Section(string key)
+ {
+ if (Json[key] is not JsonObject jsonObject)
+ {
+ jsonObject = new JsonObject();
+ Json[key] = jsonObject;
+ }
+
+ return new ProvenanceSection(owner, jsonObject, ItemPath(key));
+ }
+
+ /// Writes , or with when it is null or blank.
+ public void Set(string key, string? value, string reasonIfUnknown)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ SetUnknown(key, reasonIfUnknown);
+ return;
+ }
+
+ Json[key] = value;
+ }
+
+ /// Writes , or with when it is null.
+ public void Set(string key, JsonNode? jsonNode, string reasonIfUnknown)
+ {
+ if (jsonNode == null)
+ {
+ SetUnknown(key, reasonIfUnknown);
+ return;
+ }
+
+ Json[key] = jsonNode;
+ }
+
+ public void SetUnknown(string key, string reason)
+ {
+ Json[key] = SimulationRunProvenance.Unknown;
+
+ //The whole item is unknown now: reasons filed for its sub-items no longer describe anything in the provenance.
+ owner.ForgetUnknownUnder(ItemPath(key) + ".");
+ RecordUnknown(key, reason);
+ }
+
+ /// Files a reason under this section's item without changing its value (for a value written by someone else).
+ public void RecordUnknown(string key, string reason)
+ {
+ owner.RecordUnknown(ItemPath(key), reason);
+ }
+
+ private string ItemPath(string key)
+ {
+ return Path == null ? key : Path + "." + key;
+ }
+ }
+}
diff --git a/WPF/SAM.Analytical.UI.WPF/Classes/Tas/TasSimulationRun.cs b/WPF/SAM.Analytical.UI.WPF/Classes/Tas/TasSimulationRun.cs
new file mode 100644
index 00000000..54f16bbd
--- /dev/null
+++ b/WPF/SAM.Analytical.UI.WPF/Classes/Tas/TasSimulationRun.cs
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace SAM.Analytical.UI.WPF
+{
+ /// How one Energy Simulation / Run Simulation > Tas workflow ended.
+ public enum TasSimulationStatus
+ {
+ /// The TAS workflow ran and returned a model.
+ Succeeded,
+
+ /// The workflow started and returned no model (a TAS refusal, TAS-SIM-001, or any other failure), or threw.
+ Failed,
+
+ /// The user cancelled.
+ Cancelled,
+
+ /// The run could not start (no gbXML, a TBD that could not be replaced, ...); nothing was simulated.
+ Refused,
+ }
+
+ ///
+ /// The record of one TAS workflow started by Energy Simulation or Run Simulation > Tas (roadmap S03): which model, where its files
+ /// are, how it ended and its provenance. Written beside the TBD as <project>, overwritten with
+ /// the TBD and TSD by the next run of the same project into the same folder. It holds no result value.
+ ///
+ /// The status is the workflow's own outcome (), never read from files in the folder (S01b).
+ ///
+ ///
+ public sealed class TasSimulationRun
+ {
+ public const string Schema = "sam-ui.tas-run/1";
+
+ public const string ManifestFileSuffix = ".tas-run.json";
+
+ public string? Id { get; set; }
+
+ public TasSimulationStatus Status { get; set; } = TasSimulationStatus.Failed;
+
+ public DateTime StartedUtc { get; set; }
+
+ public DateTime FinishedUtc { get; set; }
+
+ /// The output folder: the TBD, the TSD and this record are in it.
+ public string? Directory { get; set; }
+
+ /// The open model's file, when it had one. Never written to by the run.
+ public string? SourcePath { get; set; }
+
+ public string? ProjectName { get; set; }
+
+ public string? ModelName { get; set; }
+
+ public Guid ModelGuid { get; set; }
+
+ public string? TbdPath { get; set; }
+
+ public string? TsdPath { get; set; }
+
+ /// Why the run could not start; null where it started.
+ public string? Refusal { get; set; }
+
+ /// What the workflow reported about a failure (S01b: TAS return code, error log, TAS-SIM-001).
+ public string? Failure { get; set; }
+
+ public List Notes { get; } = new List();
+
+ /// An unexpected failure, kept for the caller; only its text is written.
+ public Exception? Exception { get; set; }
+
+ public SimulationRunProvenance? Provenance { get; set; }
+
+ public string? ManifestPath { get; set; }
+
+ ///
+ /// The outcome from what the workflow itself returned: a throw or a model-less return is Failed, a refusal Refused, a cancel Cancelled,
+ /// and only a returned model Succeeded.
+ ///
+ public static TasSimulationStatus StatusOf(bool returnedModel, bool cancelled, string? refusal, Exception? exception)
+ {
+ if (exception != null)
+ {
+ return TasSimulationStatus.Failed;
+ }
+
+ if (refusal != null)
+ {
+ return TasSimulationStatus.Refused;
+ }
+
+ if (cancelled)
+ {
+ return TasSimulationStatus.Cancelled;
+ }
+
+ return returnedModel ? TasSimulationStatus.Succeeded : TasSimulationStatus.Failed;
+ }
+
+ /// The manifest's path for in .
+ public static string ManifestPathOf(string directory, string projectName)
+ {
+ return System.IO.Path.Combine(directory, projectName + ManifestFileSuffix);
+ }
+
+ public JsonObject ToJsonObject()
+ {
+ JsonArray notes = new JsonArray();
+ Notes.ForEach(x => notes.Add(JsonValue.Create(x)));
+
+ return new JsonObject
+ {
+ ["schema"] = Schema,
+ ["id"] = Id,
+ ["status"] = Status.ToString(),
+ ["startedUtc"] = StartedUtc.ToString("o"),
+ ["finishedUtc"] = FinishedUtc.ToString("o"),
+ ["durationSeconds"] = System.Math.Round((FinishedUtc - StartedUtc).TotalSeconds, 3),
+ ["sourcePath"] = SourcePath,
+ ["projectName"] = ProjectName,
+ ["model"] = new JsonObject
+ {
+ ["name"] = ModelName,
+ ["guid"] = ModelGuid.ToString("N"),
+ },
+ ["outputs"] = new JsonObject
+ {
+ ["directory"] = Directory,
+ ["tbd"] = TbdPath,
+ ["tsd"] = TsdPath,
+ },
+ ["refusal"] = Refusal,
+ ["failure"] = Failure,
+ ["notes"] = notes,
+ ["error"] = Exception == null ? null : Exception.GetType().Name + ": " + Exception.Message,
+ ["provenance"] = Provenance?.ToJsonObject(),
+ };
+ }
+
+ public string ToJson()
+ {
+ return ToJsonObject().ToJsonString(new JsonSerializerOptions { WriteIndented = true });
+ }
+ }
+}
diff --git a/WPF/SAM.Analytical.UI.WPF/Controls/SimulateControl.xaml.cs b/WPF/SAM.Analytical.UI.WPF/Controls/SimulateControl.xaml.cs
index 1d608ee9..f5bd5205 100644
--- a/WPF/SAM.Analytical.UI.WPF/Controls/SimulateControl.xaml.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Controls/SimulateControl.xaml.cs
@@ -87,6 +87,16 @@ public TextMap TextMap
}
}
+ /// The file the selected weather was read from in this dialog; null for the model's (internal) weather or one not read from a file.
+ public string? SelectedWeatherDataPath
+ {
+ get
+ {
+ string text = selectSAMObjectComboBoxControl_WeatherData.SelectedText;
+ return text != null && text != InternalText && System.IO.File.Exists(text) ? text : null;
+ }
+ }
+
public WeatherData SelectedWeatherData
{
get
diff --git a/WPF/SAM.Analytical.UI.WPF/Modify/RunEngineComparison.cs b/WPF/SAM.Analytical.UI.WPF/Modify/RunEngineComparison.cs
index aa61cfba..a560490c 100644
--- a/WPF/SAM.Analytical.UI.WPF/Modify/RunEngineComparison.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Modify/RunEngineComparison.cs
@@ -59,6 +59,7 @@ internal static EngineComparisonRun RunEngineComparison(AnalyticalModel analytic
StartedUtc = DateTime.UtcNow,
SourcePath = sourcePath,
ModelName = analyticalModel?.Name,
+ Provenance = Query.NewRunProvenance("Both"),
};
if (analyticalModel == null || string.IsNullOrWhiteSpace(directory))
@@ -91,6 +92,7 @@ internal static EngineComparisonRun RunEngineComparison(AnalyticalModel analytic
string? epwPath_User = !string.IsNullOrWhiteSpace(epwPath) && File.Exists(epwPath) ? epwPath : null;
string? epwPath_Source = epwPath_User ?? EmbeddedWeatherPath(analyticalModel_Frozen);
result.WeatherSource = epwPath_User != null ? "File" : epwPath_Source != null ? "Embedded" : "None";
+ result.WeatherOriginalPath = epwPath_User;
if (epwPath_Source == null)
{
result.Diagnostics.Add("Error: no weather - choose an EPW file, or give the model annual WeatherData; nothing was run");
@@ -134,6 +136,9 @@ internal static EngineComparisonRun RunEngineComparison(AnalyticalModel analytic
continue;
}
+ //S03: the binary about to run, described before it runs.
+ Query.AddProducerProvenance(result.Provenance, producerRun);
+
Stopwatch stopwatch = Stopwatch.StartNew();
try
{
@@ -211,6 +216,7 @@ internal static EngineComparisonRun RunEngineComparison(AnalyticalModel analytic
finally
{
result.FinishedUtc = DateTime.UtcNow;
+ CompleteRunProvenance(() => Query.CompleteEngineComparisonRunProvenance(result), result.Diagnostics);
WriteEngineComparisonManifest(result);
}
diff --git a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs
index e25ea7d9..0de519df 100644
--- a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs
@@ -8,6 +8,7 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Text.Json.Nodes;
using System.Threading;
namespace SAM.Analytical.UI.WPF
@@ -49,6 +50,7 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod
SourcePath = sourcePath,
ModelName = analyticalModel?.Name,
ModelGuid = analyticalModel?.Guid ?? Guid.Empty,
+ Provenance = Query.NewRunProvenance("OpenStudio"),
};
if (analyticalModel == null || string.IsNullOrWhiteSpace(directory))
@@ -90,6 +92,7 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod
finally
{
result.FinishedUtc = DateTime.UtcNow;
+ CompleteRunProvenance(() => Query.CompleteOpenStudioRunProvenance(result), result.Diagnostics);
WriteManifest(result);
}
@@ -155,12 +158,17 @@ private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStud
{
openStudioSimulationRun.SamOpenStudioVersion = typeof(OpenStudioConversionResult).Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(OpenStudioConversionResult).Assembly.GetName().Version?.ToString();
+ //S03: the SAM assemblies this process loaded to convert and run, read here where their types are reached anyway.
+ openStudioSimulationRun.Provenance?.AddComponent(typeof(OpenStudioConversionResult).Assembly);
+ openStudioSimulationRun.Provenance?.AddComponent(typeof(Core.OpenStudio.OpenStudioRunOptions).Assembly);
+
//The weather the converter will use, by its own precedence: the user's EPW, else the model's embedded WeatherData. The user's
//path is passed on as given (null for embedded), so the conversion is exactly SAM_OpenStudio's; the resolved file is only
//copied into the run folder, so the run keeps its weather when the original or the temporary export is gone.
string? epwPath_User = !string.IsNullOrWhiteSpace(epwPath) && File.Exists(epwPath) ? epwPath : null;
string? epwPath_Effective = epwPath_User ?? analyticalModel_Frozen.EmbeddedAnnualWeatherPath();
openStudioSimulationRun.WeatherSource = epwPath_User != null ? "File" : epwPath_Effective != null ? "Embedded" : "None";
+ openStudioSimulationRun.WeatherOriginalPath = epwPath_User;
if (epwPath_Effective != null)
{
openStudioSimulationRun.WeatherPath = Path.Combine(openStudioSimulationRun.Directory!, OpenStudioSimulationRun.WeatherFileName);
@@ -174,16 +182,37 @@ private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStud
UseUniqueRunDirectory = false,
};
+ openStudioSimulationRun.RunOptions = new JsonObject
+ {
+ ["epwPath"] = epwPath_User ?? "(none: the model's embedded WeatherData, SAM_OpenStudio's own precedence)",
+ ["outputDirectory"] = openStudioSimulationRun.Directory,
+ ["conversionOptions"] = "SAM_OpenStudio defaults",
+ ["run"] = true,
+ ["cliPath"] = openStudioRunOptions.CliPath ?? "(discovered by SAM_OpenStudio; the CLI that ran is engine.cli)",
+ ["runDirectory"] = openStudioRunOptions.RunDirectory ?? "(not set: a directory next to the OSW file)",
+ ["ddyPath"] = openStudioRunOptions.DdyPath ?? "(none)",
+ ["timeoutSeconds"] = openStudioRunOptions.TimeoutSeconds,
+ ["useUniqueRunDirectory"] = openStudioRunOptions.UseUniqueRunDirectory,
+ ["extractTimeSeries"] = openStudioRunOptions.ExtractTimeSeries,
+ };
+
OpenStudioProgress progress_Run = new OpenStudioProgress(openStudioSimulationRun, progress);
using OpenStudioConversionResult openStudioConversionResult = analyticalModel_Frozen.ToOpenStudio(epwPath_User, openStudioSimulationRun.Directory, null, openStudioRunOptions, true, progress_Run, cancellationToken);
+ //S03: asked only now that the conversion has loaded the SDK, so a missing OpenStudio stack fails in the conversion exactly as
+ //before. A version the SDK cannot give stays null and is written as unknown.
+ openStudioSimulationRun.OpenStudioSdkVersion = SafeVersion(Core.OpenStudio.Query.OpenStudioVersion);
+ openStudioSimulationRun.EnergyPlusSdkVersion = SafeVersion(Core.OpenStudio.Query.EnergyPlusVersion);
+
if (openStudioConversionResult == null)
{
openStudioSimulationRun.Diagnostics.Add("Error: SAM_OpenStudio returned no conversion result");
return;
}
+ openStudioSimulationRun.DesignDaySource = openStudioConversionResult.DesignDaySource.ToString();
+
foreach (Core.OpenStudio.OpenStudioDiagnostic openStudioDiagnostic in openStudioConversionResult.Diagnostics)
{
if (openStudioDiagnostic != null)
@@ -248,6 +277,36 @@ private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStud
openStudioSimulationRun.Status = OpenStudioSimulationStatus.Succeeded;
}
+ /// A version from the OpenStudio SDK, or null when the SDK cannot give one.
+ private static string? SafeVersion(Func version)
+ {
+ try
+ {
+ string value = version();
+ return string.IsNullOrWhiteSpace(value) ? null : value;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Completes a run's provenance (S03) from a finally. Collecting provenance never changes how a run ended: a failure here is a warning
+ /// in the run's diagnostics, and the manifest is still written.
+ ///
+ internal static void CompleteRunProvenance(Action complete, List diagnostics)
+ {
+ try
+ {
+ complete();
+ }
+ catch (Exception exception)
+ {
+ diagnostics.Add("Warning: the run provenance could not be completed: " + exception.GetType().Name + ": " + exception.Message);
+ }
+ }
+
private static void WriteManifest(OpenStudioSimulationRun openStudioSimulationRun)
{
if (string.IsNullOrWhiteSpace(openStudioSimulationRun?.Directory) || !System.IO.Directory.Exists(openStudioSimulationRun!.Directory))
diff --git a/WPF/SAM.Analytical.UI.WPF/Modify/RunPartOSimulation.cs b/WPF/SAM.Analytical.UI.WPF/Modify/RunPartOSimulation.cs
index a42c8892..f62cc18f 100644
--- a/WPF/SAM.Analytical.UI.WPF/Modify/RunPartOSimulation.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Modify/RunPartOSimulation.cs
@@ -137,6 +137,16 @@ public static AnalyticalModel RunPartOSimulation(AnalyticalModel analyticalModel
/// (which has no way to say).
///
public static AnalyticalModel RunPartOSimulation(AnalyticalModel analyticalModel, PartOSimulationContext partOSimulationContext, string projectName, PartORun partORun, CancellationToken externalCancellationToken, out string path_TBD, out string path_TSD, out bool cancelled, out bool fullYear, out List notes, out string refusal, out string? failure, PartOCanonicalTBD partOCanonicalTBD = null, PartOWorkflowRunner partOWorkflowRunner = null)
+ {
+ return RunPartOSimulation(analyticalModel, partOSimulationContext, projectName, partORun, externalCancellationToken, out path_TBD, out path_TSD, out cancelled, out fullYear, out notes, out refusal, out failure, out WorkflowSettings? _, partOCanonicalTBD, partOWorkflowRunner);
+ }
+
+ ///
+ /// The same run, also handing back the it gave the workflow (S03: the run manifest records the settings
+ /// that ran, not the ones asked for).
+ ///
+ /// The settings handed to the workflow; null where the run stopped before they were built.
+ internal static AnalyticalModel RunPartOSimulation(AnalyticalModel analyticalModel, PartOSimulationContext partOSimulationContext, string projectName, PartORun partORun, CancellationToken externalCancellationToken, out string path_TBD, out string path_TSD, out bool cancelled, out bool fullYear, out List notes, out string refusal, out string? failure, out WorkflowSettings? workflowSettings, PartOCanonicalTBD partOCanonicalTBD = null, PartOWorkflowRunner partOWorkflowRunner = null)
{
path_TBD = null;
path_TSD = null;
@@ -145,6 +155,7 @@ public static AnalyticalModel RunPartOSimulation(AnalyticalModel analyticalModel
notes = [];
refusal = null;
failure = null;
+ workflowSettings = null;
if (analyticalModel is null || partOSimulationContext is null || string.IsNullOrWhiteSpace(projectName))
{
@@ -483,7 +494,7 @@ public static AnalyticalModel RunPartOSimulation(AnalyticalModel analyticalModel
simulate = true;
}
- WorkflowSettings workflowSettings = new()
+ workflowSettings = new()
{
Path_TBD = path_TBD,
diff --git a/WPF/SAM.Analytical.UI.WPF/Modify/Simulate.cs b/WPF/SAM.Analytical.UI.WPF/Modify/Simulate.cs
index 42d8be35..793fa82a 100644
--- a/WPF/SAM.Analytical.UI.WPF/Modify/Simulate.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Modify/Simulate.cs
@@ -11,6 +11,7 @@
using SAM.Weather;
using System;
using System.Collections.Generic;
+using System.Text.Json.Nodes;
using System.Threading;
using System.Windows.Forms;
@@ -152,6 +153,7 @@ public static void Simulate(this UIAnalyticalModel uIAnalyticalModel, PartORun p
UpdateConstructionLayersByPanelType = simulateWindow.UpdateConstructionLayersByPanelType,
TextMap = simulateWindow.SelectedTextMap,
WeatherData = simulateWindow.SelectedWeatherData,
+ WeatherFilePath = simulateWindow.SelectedWeatherDataPath,
ZoneCategory = simulateWindow.SelectedZoneCategory,
Simulate = simulateWindow.Simulate,
@@ -263,6 +265,78 @@ internal static PartOSimulationOutcome SimulatePartO(UIAnalyticalModel uIAnalyti
return Simulate(uIAnalyticalModel, partORun, analyticalModel, simulateInputs, true, partOWorkflowRunner);
}
+ /// Keeps a TAS run's exception on its record (S03) and returns false, so an exception filter that calls it never catches.
+ private static bool RecordException(TasSimulationRun? tasSimulationRun, Exception exception)
+ {
+ if (tasSimulationRun is not null)
+ {
+ tasSimulationRun.Exception = exception;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Writes a TAS run's record beside its TBD (S03), its provenance collected by . Called from a finally:
+ /// neither collecting the provenance nor writing the file may change how the run ended, so a failure of either is only a note on the
+ /// record (and on the record's absence, nothing).
+ ///
+ internal static void WriteTasSimulationManifest(TasSimulationRun tasSimulationRun, Func provenance)
+ {
+ tasSimulationRun.FinishedUtc = DateTime.UtcNow;
+
+ try
+ {
+ tasSimulationRun.Provenance = provenance();
+ }
+ catch (Exception exception)
+ {
+ tasSimulationRun.Notes.Add("Warning: the run provenance could not be collected: " + exception.GetType().Name + ": " + exception.Message);
+ }
+
+ if (string.IsNullOrWhiteSpace(tasSimulationRun.Directory) || string.IsNullOrWhiteSpace(tasSimulationRun.ProjectName) || !System.IO.Directory.Exists(tasSimulationRun.Directory))
+ {
+ return;
+ }
+
+ string path = TasSimulationRun.ManifestPathOf(tasSimulationRun.Directory!, tasSimulationRun.ProjectName!);
+ try
+ {
+ System.IO.File.WriteAllText(path, tasSimulationRun.ToJson());
+ tasSimulationRun.ManifestPath = path;
+ }
+ catch (Exception exception)
+ {
+ tasSimulationRun.Notes.Add("Warning: the run record could not be written: " + exception.GetType().Name + ": " + exception.Message);
+ }
+ }
+
+ ///
+ /// The ordinary command's core (Energy Simulation / Run Simulation > Tas: no Part O run, no case folder), quiet, with the dialog's
+ /// weather given and the TAS workflow step supplied - for a test of what that core records (S03). Full year, sizing and unmet hours,
+ /// the TAS solar calculation with Direct T3D, no exports. Null is TAS.
+ ///
+ internal static PartOSimulationOutcome Simulate(UIAnalyticalModel uIAnalyticalModel, string projectName, string outputDirectory, WeatherData weatherData, string? weatherFilePath, PartOWorkflowRunner partOWorkflowRunner)
+ {
+ SimulateInputs simulateInputs = new()
+ {
+ ProjectName = projectName,
+ OutputDirectory = outputDirectory,
+ UnmetHours = true,
+ FullYearSimulation = true,
+ FullYearSimulation_From = 1,
+ FullYearSimulation_To = 365,
+ Sizing = true,
+ SolarCalculationMethod = SolarCalculationMethod.TAS,
+ DirectT3D = true,
+ WeatherData = weatherData,
+ WeatherFilePath = weatherFilePath,
+ Simulate = true,
+ };
+
+ return Simulate(uIAnalyticalModel, null, uIAnalyticalModel?.JSAMObject, simulateInputs, true, partOWorkflowRunner);
+ }
+
/// What the Simulate dialog hands back, as values - so the dialog path and the Hub path share one core.
private sealed class SimulateInputs
{
@@ -284,6 +358,10 @@ private sealed class SimulateInputs
public bool UpdateConstructionLayersByPanelType;
public TextMap TextMap;
public WeatherData WeatherData;
+
+ /// The file was read from in the Simulate dialog; null for the model's or a remembered weather (S03 provenance only).
+ public string WeatherFilePath;
+
public string ZoneCategory;
public bool Simulate;
@@ -493,7 +571,54 @@ private static PartOSimulationOutcome Simulate(UIAnalyticalModel uIAnalyticalMod
// the zones, the shading, the workflow - and the Part O arming that goes before it. Extracted
// so an Iteration 2B optimisation can repeat this exact case over a changed design without a
// second copy of it existing to drift. See Modify.RunPartOSimulation.
- AnalyticalModel analyticalModel_Workflow = Modify.RunPartOSimulation(analyticalModel, partOSimulationContext, projectName, partORun, CancellationToken.None, out path_TBD, out string _, out cancelled, out workflowSimulatedFullYear, out List notes_Simulation, out string refusal_Simulation, out failure_Simulation, null, partOWorkflowRunner);
+ // S03: the ordinary command (Energy Simulation, Run Simulation > Tas) records each run - its outcome, files and provenance -
+ // beside the TBD. Not the Part O routes, whose case folders and run records are their own.
+ TasSimulationRun? tasSimulationRun = partORun is null && simulateInputs.PartOOutputCase is null
+ ? new TasSimulationRun
+ {
+ Id = Guid.NewGuid().ToString("N").Substring(0, 8),
+ StartedUtc = DateTime.UtcNow,
+ Directory = outputDirectory,
+ SourcePath = uIAnalyticalModel.Path,
+ ProjectName = projectName,
+ ModelName = analyticalModel_Source.Name,
+ ModelGuid = analyticalModel_Source.Guid,
+ }
+ : null;
+
+ AnalyticalModel analyticalModel_Workflow = null;
+ List notes_Simulation = null;
+ string refusal_Simulation = null;
+ string path_TSD_Run = null;
+ WorkflowSettings? workflowSettings_Run = null;
+ try
+ {
+ analyticalModel_Workflow = Modify.RunPartOSimulation(analyticalModel, partOSimulationContext, projectName, partORun, CancellationToken.None, out path_TBD, out path_TSD_Run, out cancelled, out workflowSimulatedFullYear, out notes_Simulation, out refusal_Simulation, out failure_Simulation, out workflowSettings_Run, null, partOWorkflowRunner);
+ }
+ catch (Exception exception) when (RecordException(tasSimulationRun, exception))
+ {
+ // Never reached: the filter only records the exception and returns false, so it propagates exactly as before.
+ throw;
+ }
+ finally
+ {
+ if (tasSimulationRun is not null)
+ {
+ tasSimulationRun.TbdPath = path_TBD != null && System.IO.File.Exists(path_TBD) ? path_TBD : null;
+ tasSimulationRun.TsdPath = path_TSD_Run != null && System.IO.File.Exists(path_TSD_Run) ? path_TSD_Run : null;
+ tasSimulationRun.Refusal = refusal_Simulation;
+ tasSimulationRun.Failure = failure_Simulation;
+ if (notes_Simulation is not null)
+ {
+ tasSimulationRun.Notes.AddRange(notes_Simulation);
+ }
+
+ tasSimulationRun.Status = TasSimulationRun.StatusOf(analyticalModel_Workflow is not null, cancelled, refusal_Simulation, tasSimulationRun.Exception);
+
+ JsonObject inputs = Query.TasRunInputs(simulateInputs.Simulate, fullYearSimulation, fullYearSimulation_From, fullYearSimulation_To, sizing, unmetHours, useWidths, solarCalculationMethod, simulateInputs.DirectT3D, updateConstructionLayersByPanelType, createTM59, createSAP, createTPD, createPartL, printRoomDataSheets, zoneCategory);
+ WriteTasSimulationManifest(tasSimulationRun, () => Query.TasRunProvenance(analyticalModel_Source, weatherData, simulateInputs.WeatherFilePath, workflowSettings_Run, inputs, SimulationRunProvenance.TasComServerCommand()));
+ }
+ }
notes_Simulate.AddRange(notes_Simulation);
diff --git a/WPF/SAM.Analytical.UI.WPF/Query/SimulationRunProvenance.cs b/WPF/SAM.Analytical.UI.WPF/Query/SimulationRunProvenance.cs
new file mode 100644
index 00000000..9576c423
--- /dev/null
+++ b/WPF/SAM.Analytical.UI.WPF/Query/SimulationRunProvenance.cs
@@ -0,0 +1,308 @@
+// SPDX-License-Identifier: LGPL-3.0-or-later
+// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors
+
+using SAM.Analytical.Tas;
+using SAM.Weather;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Nodes;
+
+namespace SAM.Analytical.UI.WPF
+{
+ ///
+ /// How each route fills its (roadmap S03). One mechanism for the three routes; each only adds
+ /// what its engine actually received.
+ ///
+ public static partial class Query
+ {
+ /// A new provenance for ; null if even that fails, so starting a run never depends on it.
+ public static SimulationRunProvenance? NewRunProvenance(string route)
+ {
+ try
+ {
+ return new SimulationRunProvenance(route);
+ }
+ catch (System.Exception)
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// The provenance of one Energy Simulation / Run Simulation > Tas workflow.
+ ///
+ /// The model handed to the run, before any change; its embedded WeatherData tells embedded weather from explicit.
+ /// The weather the run gave TAS.
+ /// The file that weather was read from in the Simulate dialog, when it was; null for the embedded or a remembered weather.
+ /// The settings handed to WorkflowCalculator; null when the run stopped before they were built.
+ /// The Simulate dialog's inputs as handed to the run (exports, solar method, ...).
+ /// The registered TBD.Document server ().
+ public static SimulationRunProvenance TasRunProvenance(AnalyticalModel? analyticalModel_Source, WeatherData? weatherData, string? weatherFilePath, WorkflowSettings? workflowSettings, JsonObject? inputs, string? tasComServerCommand)
+ {
+ SimulationRunProvenance result = new SimulationRunProvenance("Tas");
+ ProvenanceSection root = result.Root;
+
+ SimulationRunProvenance.Guard(root, "engine", () =>
+ {
+ result.AddComponent(typeof(WorkflowSettings).Assembly);
+ SimulationRunProvenance.SetTasEngine(root.Section("engine"), tasComServerCommand);
+ });
+
+ string? weatherSha256 = null;
+ SimulationRunProvenance.Guard(root, "weather", () => weatherSha256 = SetTasWeather(root.Section("weather"), analyticalModel_Source, weatherData, weatherFilePath));
+ SimulationRunProvenance.Guard(root, "designDays", () => SetTasDesignDays(root.Section("designDays"), weatherData, workflowSettings));
+
+ SimulationRunProvenance.Guard(root, "settings", () =>
+ {
+ ProvenanceSection settings = root.Section("settings");
+ settings.Set("inputs", inputs?.DeepClone(), "the Simulate inputs were not recorded");
+
+ JsonObject? workflow = workflowSettings?.ToJsonObject();
+ if (workflow != null && workflow["WeatherData"] != null)
+ {
+ //The weather is identified, not repeated: it is the weather above, all 8760 hours of it.
+ workflow["WeatherData"] = new JsonObject { ["name"] = weatherData?.Name, ["contentSha256"] = weatherSha256 };
+ }
+
+ settings.Set("workflow", workflow, "the run stopped before the TAS workflow settings were built (see refusal, failure or error)");
+ });
+
+ return result;
+ }
+
+ /// The weather of a Tas run: its source (embedded or explicit), name, content hash and, when read from a file, that file's hash. Returns the content hash.
+ private static string? SetTasWeather(ProvenanceSection section, AnalyticalModel? analyticalModel_Source, WeatherData? weatherData, string? weatherFilePath)
+ {
+ if (weatherData == null)
+ {
+ section.Json["source"] = "None";
+ section.SetUnknown("contentSha256", "the run was given no weather");
+ return null;
+ }
+
+ string contentSha256 = SimulationRunProvenance.WeatherContentSha256(weatherData);
+
+ //Embedded means the very weather the model carries, by content: a remembered or chosen weather that happens to be labelled
+ //as the model's is not.
+ string? embeddedSha256 = null;
+ if (analyticalModel_Source != null && analyticalModel_Source.TryGetValue(Analytical.AnalyticalModelParameter.WeatherData, out WeatherData weatherData_Embedded) && weatherData_Embedded != null)
+ {
+ embeddedSha256 = SimulationRunProvenance.WeatherContentSha256(weatherData_Embedded);
+ }
+
+ section.Json["source"] = embeddedSha256 == contentSha256 ? "Embedded" : "Explicit";
+ section.Set("name", weatherData.Name, "the weather has no name");
+ section.Json["contentSha256"] = contentSha256;
+ section.Json["contentHashOf"] = "the SAM WeatherData JSON given to TAS without its own Guid, canonical JSON v1 (SAM_Validation)";
+
+ if (!string.IsNullOrWhiteSpace(weatherFilePath) && System.IO.File.Exists(weatherFilePath))
+ {
+ ProvenanceSection file = section.Section("file");
+ file.Json["path"] = weatherFilePath;
+ file.Set("sha256", SimulationRunProvenance.Sha256(weatherFilePath!), "the weather file could not be read");
+ }
+ else
+ {
+ section.SetUnknown("file", embeddedSha256 == contentSha256
+ ? "the weather is the model's embedded WeatherData, not a file; contentSha256 identifies it"
+ : "the weather was not read from a file in this Simulate dialog (a remembered weather); contentSha256 identifies it");
+ }
+
+ return contentSha256;
+ }
+
+ /// The design days a Tas run handed TAS, and whether they are the ones derived from its weather.
+ private static void SetTasDesignDays(ProvenanceSection section, WeatherData? weatherData, WorkflowSettings? workflowSettings)
+ {
+ if (workflowSettings == null)
+ {
+ section.SetUnknown("source", "the run stopped before the TAS workflow settings were built");
+ return;
+ }
+
+ List heating = workflowSettings.DesignDays_Heating ?? new List();
+ List cooling = workflowSettings.DesignDays_Cooling ?? new List();
+
+ bool derived = weatherData != null
+ && SameDesignDays(heating, Analytical.Query.HeatingDesignDay(weatherData))
+ && SameDesignDays(cooling, Analytical.Query.CoolingDesignDay(weatherData));
+
+ if (derived)
+ {
+ section.Json["source"] = "DerivedFromWeather";
+ }
+ else
+ {
+ section.SetUnknown("source", "the design days handed to TAS are not the ones derived from the run's weather, so where they came from could not be established");
+ }
+
+ section.Json["heating"] = Names(heating);
+ section.Json["cooling"] = Names(cooling);
+ section.Json["usedForSizing"] = workflowSettings.Sizing;
+ }
+
+ private static bool SameDesignDays(List designDays, DesignDay? designDay_Derived)
+ {
+ return designDay_Derived != null && designDays.Count == 1 && designDays[0] != null
+ && designDays[0].ToJsonObject().ToJsonString() == designDay_Derived.ToJsonObject().ToJsonString();
+ }
+
+ private static JsonArray Names(IEnumerable designDays)
+ {
+ JsonArray result = new JsonArray();
+ foreach (DesignDay designDay in designDays.Where(x => x != null))
+ {
+ result.Add(JsonValue.Create(designDay.Name ?? string.Empty));
+ }
+
+ return result;
+ }
+
+ ///
+ /// The Simulate dialog's inputs as one object, so the manifest shows what was asked for beside the workflow settings that ran.
+ ///
+ public static JsonObject TasRunInputs(bool simulate, bool fullYearSimulation, int fullYearSimulation_From, int fullYearSimulation_To, bool sizing, bool unmetHours, bool useWidths, SolarCalculationMethod solarCalculationMethod, bool directT3D, bool updateConstructionLayersByPanelType, bool createTM59, bool createSAP, bool createTPD, bool createPartL, bool printRoomDataSheets, string? zoneCategory)
+ {
+ return new JsonObject
+ {
+ ["simulate"] = simulate,
+ ["fullYearSimulation"] = fullYearSimulation,
+ ["fullYearSimulationFrom"] = fullYearSimulation_From,
+ ["fullYearSimulationTo"] = fullYearSimulation_To,
+ ["sizing"] = sizing,
+ ["unmetHours"] = unmetHours,
+ ["useWidths"] = useWidths,
+ ["solarCalculationMethod"] = solarCalculationMethod.ToString(),
+ ["directT3D"] = directT3D,
+ ["updateConstructionLayersByPanelType"] = updateConstructionLayersByPanelType,
+ ["createTM59"] = createTM59,
+ ["createSAP"] = createSAP,
+ ["createTPD"] = createTPD,
+ ["createPartL"] = createPartL,
+ ["printRoomDataSheets"] = printRoomDataSheets,
+ ["zoneCategory"] = zoneCategory ?? "(none)",
+ };
+ }
+
+ /// The producer about to run in a Run Simulation > Both: its executable (path, SHA-256, versions) and the arguments it is given.
+ public static void AddProducerProvenance(SimulationRunProvenance? provenance, EngineComparisonProducerRun producerRun)
+ {
+ if (provenance == null || producerRun == null)
+ {
+ return;
+ }
+
+ ProvenanceSection producer = provenance.Root.Section("producers").Section(producerRun.Engine.ToString());
+ SimulationRunProvenance.Guard(producer, "executable", () => SimulationRunProvenance.SetExecutable(producer.Section("executable"), producerRun.ExecutablePath));
+
+ JsonArray arguments = new JsonArray();
+ producerRun.Arguments.ForEach(x => arguments.Add(JsonValue.Create(x)));
+ producer.Json["arguments"] = arguments;
+ }
+
+ ///
+ /// Completes a Run Simulation > Both provenance once the run has ended: the SAM_Validation build that compared, the commit overrides
+ /// the producers inherited, the weather, and what each producer reported in its own B1a document (its SAM and runner commits, engine and
+ /// version, weather and design-day source). A producer that never ran is recorded as unknown with the reason.
+ ///
+ public static void CompleteEngineComparisonRunProvenance(EngineComparisonRun engineComparisonRun)
+ {
+ SimulationRunProvenance? provenance = engineComparisonRun?.Provenance;
+ if (provenance == null)
+ {
+ return;
+ }
+
+ ProvenanceSection root = provenance.Root;
+
+ SimulationRunProvenance.Guard(root, "application", () =>
+ {
+ provenance.AddComponent(typeof(Benchmark.BenchmarkCanonicalJson).Assembly);
+ provenance.AddComponent(typeof(Benchmark.Compare.ComparisonResult).Assembly);
+ });
+
+ //The producers resolve samCommit and runnerCommit from these first, before their own build stamp: a value here is a manual
+ //override, and the reported commits below came from it.
+ SimulationRunProvenance.Guard(root, "environment", () => SimulationRunProvenance.SetEnvironment(root.Section("environment"), "SAM_COMMIT", "RUNNER_COMMIT"));
+
+ SimulationRunProvenance.Guard(root, "weather", () =>
+ {
+ ProvenanceSection weather = root.Section("weather");
+ weather.Set("source", engineComparisonRun!.WeatherSource, "the run stopped before the weather was resolved");
+ weather.Set("sha256", engineComparisonRun.WeatherSha256, engineComparisonRun.WeatherSource == "None" ? "the run had no weather file" : "the run stopped before the weather file was copied");
+ weather.Set("originalPath", engineComparisonRun.WeatherOriginalPath, engineComparisonRun.WeatherSource == "Embedded"
+ ? "the weather is the model's embedded WeatherData, exported to EPW once for both producers; sha256 identifies the exported file"
+ : "no weather file was chosen");
+ });
+
+ root.Section("designDays").Json["source"] = "chosen by each producer from the snapshot (SAM_UI passes none): see producers..reported.designDaySource";
+ root.Section("settings").Json["producers"] = "each producer's arguments (producers..arguments) and its built-in workflow settings, which its executable SHA-256 pins";
+
+ foreach (EngineComparisonProducerRun producerRun in engineComparisonRun!.ProducerRuns)
+ {
+ ProvenanceSection producer = root.Section("producers").Section(producerRun.Engine.ToString());
+ if (producer.Json["executable"] == null)
+ {
+ producer.SetUnknown("executable", "the producer did not run: " + (producerRun.Error ?? "no reason was recorded"));
+ }
+
+ producer.Json["succeeded"] = producerRun.Succeeded;
+
+ //A failed producer's document is kept for diagnosis (S01b); its provenance is reported as written, never used as a result.
+ string? benchmarkPath = producerRun.BenchmarkPath ?? ExpectedBenchmarkPath(engineComparisonRun, producerRun.Engine);
+ SimulationRunProvenance.Guard(producer, "reported", () => SimulationRunProvenance.SetReportedProvenance(producer, "reported", benchmarkPath));
+ }
+ }
+
+ private static string? ExpectedBenchmarkPath(EngineComparisonRun engineComparisonRun, ComparisonEngine comparisonEngine)
+ {
+ return string.IsNullOrWhiteSpace(engineComparisonRun.Directory) ? null : System.IO.Path.Combine(engineComparisonRun.Directory!, comparisonEngine == ComparisonEngine.Tas ? TasBenchmarkFileName : OpenStudioBenchmarkFileName);
+ }
+
+ ///
+ /// Completes an OpenStudio run's provenance from its record once the run has ended: the CLI that ran, the EnergyPlus version in its
+ /// eplusout.err, the weather, the design-day source the converter established and the run options it was given.
+ ///
+ public static void CompleteOpenStudioRunProvenance(OpenStudioSimulationRun openStudioSimulationRun)
+ {
+ SimulationRunProvenance? provenance = openStudioSimulationRun?.Provenance;
+ if (provenance == null)
+ {
+ return;
+ }
+
+ ProvenanceSection root = provenance.Root;
+
+ SimulationRunProvenance.Guard(root, "engine", () =>
+ {
+ ProvenanceSection engine = root.Section("engine");
+ string notLoaded = "the OpenStudio SDK was not loaded for this run (see error and diagnostics)";
+ engine.Set("openStudioSdkVersion", openStudioSimulationRun!.OpenStudioSdkVersion, notLoaded);
+ engine.Set("energyPlusVersionOfSdk", openStudioSimulationRun.EnergyPlusSdkVersion, notLoaded);
+ engine.Set("energyPlusVersion", SimulationRunProvenance.EnergyPlusVersionFromErr(openStudioSimulationRun.ErrorFilePath), "EnergyPlus wrote no eplusout.err with its version line (it did not run, or the run stopped before it)");
+
+ if (string.IsNullOrWhiteSpace(openStudioSimulationRun.CliPath))
+ {
+ engine.SetUnknown("cli", "openstudio.exe was not started for this run");
+ }
+ else
+ {
+ SimulationRunProvenance.SetExecutable(engine.Section("cli"), openStudioSimulationRun.CliPath);
+ }
+ });
+
+ SimulationRunProvenance.Guard(root, "weather", () =>
+ {
+ ProvenanceSection weather = root.Section("weather");
+ weather.Set("source", openStudioSimulationRun!.WeatherSource, "the run stopped before the weather was resolved");
+ weather.Set("sha256", openStudioSimulationRun.WeatherSha256, openStudioSimulationRun.WeatherSource == "None" ? "the run had no weather file" : "the run stopped before the weather file was copied");
+ weather.Set("originalPath", openStudioSimulationRun.WeatherOriginalPath, openStudioSimulationRun.WeatherSource == "Embedded"
+ ? "the weather is the model's embedded WeatherData, exported to EPW by SAM_OpenStudio; sha256 identifies the exported file"
+ : "no weather file was chosen");
+ });
+
+ SimulationRunProvenance.Guard(root, "designDays", () => root.Section("designDays").Set("source", openStudioSimulationRun!.DesignDaySource, "SAM_OpenStudio returned no conversion result, so the design-day basis it established is not known"));
+ SimulationRunProvenance.Guard(root, "settings", () => root.Section("settings").Set("runOptions", openStudioSimulationRun!.RunOptions?.DeepClone(), "the run stopped before the OpenStudio run options were built"));
+ }
+ }
+}
diff --git a/WPF/SAM.Analytical.UI.WPF/Windows/SimulateWindow.xaml.cs b/WPF/SAM.Analytical.UI.WPF/Windows/SimulateWindow.xaml.cs
index 761b8d73..be562b6d 100644
--- a/WPF/SAM.Analytical.UI.WPF/Windows/SimulateWindow.xaml.cs
+++ b/WPF/SAM.Analytical.UI.WPF/Windows/SimulateWindow.xaml.cs
@@ -39,6 +39,14 @@ public WeatherData WeatherData
}
}
+ public string? SelectedWeatherDataPath
+ {
+ get
+ {
+ return simulateControl.SelectedWeatherDataPath;
+ }
+ }
+
public WeatherData SelectedWeatherData
{
get
diff --git a/documentation/S03-RunProvenance-PR.md b/documentation/S03-RunProvenance-PR.md
new file mode 100644
index 00000000..860eb0ee
--- /dev/null
+++ b/documentation/S03-RunProvenance-PR.md
@@ -0,0 +1,167 @@
+# S03 - Run settings and version traceability - PR record
+
+Roadmap: SAM `documentation/SIMULATION_ROADMAP_Q4_2026.md`, task S03 (Phase A). SAM_UI only; no SAM_Validation schema change.
+
+Base: `sow/2026-Q4` `4d7f5bd8` (S02 closeout; S01 SAM_Tas#103 / SAM_UI#240 and S02 SAM_UI#241 merged).
+Branch: `feature/s03-simulation-provenance`. One PR, not merged. **Validated content SHA: `87126efc`** (code and tests, after the review fixes; first content `29926a2f`).
+Record commits follow it. The final PR head and hosted checks are in the PR description. `PROJECT_PROGRESS.md` is not
+touched (post-merge closeout only).
+
+## Status
+Implementation and local tests complete on 2026-10-19 (overnight owner-authorised S03+S04 session). Awaiting hosted CI
+and owner review. Not merged.
+
+## Problem (baseline `4d7f5bd8`)
+- Tas only (Energy Simulation, Run Simulation > Tas) wrote no run record at all.
+- `openstudio-run.json` named the CLI path and the SAM.Analytical.OpenStudio version, but not the CLI's identity, the
+ OpenStudio/EnergyPlus versions that ran, the design-day source or the run options.
+- `engine-comparison-run.json` (Both) named each producer's path, not its binary. The SAM commit came only from the
+ producers' B1a documents, which in a developer build require `SAM_COMMIT` to be set by hand.
+- No manifest identified the SAM_UI build that ran.
+
+## Change
+- **`Classes/Provenance/SimulationRunProvenance.cs` (new):** one provenance mechanism for the three routes, written into
+ each manifest as `"provenance"` (`sam-ui.run-provenance/1`, versioned independently of the manifests).
+ - **Build:** `application.sourceRevision` is the build's own stamp, the `+` metadata of SAM.Analytical.UI.WPF's
+ InformationalVersion, which SAM_Deploy installers set to their SAM_Deploy commit. It is never read from `SAM_COMMIT`
+ or a Git HEAD. `application.components` gives name, version, InformationalVersion, path and SHA-256 of every SAM
+ assembly the run used (SAM.Core, SAM.Analytical, SAM.Analytical.UI.WPF, plus SAM.Analytical.Tas, SAM.Analytical.OpenStudio /
+ SAM.Core.OpenStudio or SAM_Validation's Benchmark assemblies per route). The hashes identify an unstamped developer build.
+ - **Unknown items:** written as `"unknown"`, with the reason under `provenance.unknown["- "]`. No item is blank.
+ - **Never changes an outcome:** each collector catches its own failure and records it as unknown; the routes complete the
+ provenance from their `finally` and a failure there is only a warning in the run's diagnostics/notes.
+- **Tas only (`Modify/Simulate.cs`, `Classes/Tas/TasSimulationRun.cs` new):** the ordinary command's core writes
+ `.tas-run.json` (`sam-ui.tas-run/1`) beside the TBD on every outcome: Succeeded, Failed, Cancelled, Refused, and
+ a thrown exception (recorded through an exception filter, so it still propagates unchanged). The status is the workflow's
+ own return (`TasSimulationRun.StatusOf`), never read from files (S01b). Provenance:
+ - engine: the registered `TBD.Document` COM server (`LocalServer32`), its product version (the TAS version) and SHA-256;
+ - weather: `Embedded` only when the weather given to TAS has the same content hash (SAM WeatherData without its own Guid, canonical JSON v1)
+ as the model's embedded weather, else `Explicit`; the file it was read from in the Simulate dialog, with its SHA-256;
+ - design days: `DerivedFromWeather` only when the days handed to TAS equal those derived from the run's weather;
+ - settings: the `WorkflowSettings` actually handed to `WorkflowCalculator` (new internal overload of
+ `RunPartOSimulation` returns them; existing overloads delegate unchanged), with the weather identified rather than
+ repeated, plus the Simulate dialog's inputs.
+ - Not written for the Part O routes (Hub, guided Part O), whose case folders and records are their own.
+- **OpenStudio only (`Modify/RunOpenStudioSimulation.cs`):** engine `openStudioSdkVersion` and `energyPlusVersionOfSdk`
+ (read after the conversion, so a missing OpenStudio stack still fails exactly as before), `energyPlusVersion` from the
+ first line of the run's `eplusout.err`, and the CLI that ran (path, SHA-256, file/product version); weather source,
+ hash and original path; the design-day source the converter established; the `OpenStudioRunOptions` and conversion
+ arguments given.
+- **Both (`Modify/RunEngineComparison.cs`):** each producer's executable (path, SHA-256, versions; for a .NET apphost also its
+ `.dll`) recorded before it runs, with its arguments; after the run, what each producer reported in its own B1a document
+ (`samCommit`, `runnerCommit`, engine, route, weather, `designDaySource`, model hashes, state), copied as written. A
+ document that is missing, unreadable or whose provenance is not an object (the S01b regression shapes) is recorded as
+ unknown with its reason. `environment.SAM_COMMIT` / `RUNNER_COMMIT` record a manual override the producers inherited
+ ("not set" otherwise).
+- **Unchanged:** run statuses, exceptions, cancellation, output files and folders, simulation settings, conversion,
+ comparison, B1a documents and schemas, SAM_Tas, SAM_OpenStudio, SAM_Validation. Every pre-S03 manifest key is kept with
+ the same JSON kind; `provenance` is additive and the manifest schema ids are unchanged.
+
+## Decisions and assumptions
+- **No Git HEAD, no SAM_COMMIT for SAM_UI's own identity.** A developer build carries no source revision, so it is reported
+ unknown with that reason, and its assembly hashes identify it. An installed build is identified by its stamp.
+- **Producers' `samCommit` is still the producers' own.** benchmark-tas / benchmark-openstudio resolve it from
+ `SAM_COMMIT`, then their own build stamp; their B1a validation rejects a missing one. SAM_UI does not pass a value it
+ cannot verify. So a *developer-built* producer still needs `SAM_COMMIT` (now recorded as a manual override in the
+ manifest); an installed, stamped producer (roadmap S06) does not. Owner decision listed below.
+- **Weather content hash** for Tas: TAS receives a SAM `WeatherData`, not a file; its canonical JSON v1 hash
+ (SAM_Validation's helper) identifies it. The file hash is added when it was read from a file in the dialog.
+- `.tas-run.json` is overwritten with the TBD/TSD by the next run of the same project into the same folder, as
+ those files are.
+
+## Tests
+- New `SimulationRunProvenanceTests.cs` (46): build stamp parsing; SAM assemblies identified by path and SHA-256;
+ `SAM_COMMIT` set in the environment is not used and an unstamped build is unknown with its reason; executable identity
+ (hash, versions, apphost `.dll`), missing and version-less files; the TAS COM server (registered, versioned, version-less,
+ not registered) and command-line parsing; EnergyPlus version from `eplusout.err`; Tas embedded weather + derived design days
+ + the workflow settings, explicit weather from a file, a remembered weather, a run stopped before the workflow, design days
+ not derived; Tas status mapping (incl. exception and cancel); the Tas record for a failed / cancelled / throwing run, a
+ provenance collector that throws (status unchanged, record written), a missing folder; Both: every producer's binary and
+ reported provenance without `SAM_COMMIT`, a manual `SAM_COMMIT` override, the five S01b malformed-document shapes
+ (still PartiallyCompleted, OpenStudio kept), a producer not found, cancel and a throw; OpenStudio failed run; historical
+ compatibility against committed pre-S03 S01b evidence (`engine-comparison-run.json`) and the pre-S03 OpenStudio key set.
+ Each test checks that every `unknown` has a reason and no item is null or blank.
+- New `TasRunManifestTests.cs` (6, WPF collection): the real ordinary Tas core, with the TAS workflow step faked through its
+ existing seam: Succeeded (settings recorded equal the settings the workflow received), explicit weather file hash,
+ Failed whatever files are in the folder, Cancelled, a thrown exception still propagates and is recorded, Refused (locked
+ TBD) with its reason.
+- `OpenStudioSimulationTests.RealRun_…` (existing, runs where OpenStudio is installed) now also asserts the provenance of a
+ real run: CLI hash, EnergyPlus version from `eplusout.err`, SDK version, design-day source, weather, run options.
+
+## Validation
+Local results on this laptop. Dependencies: SAM `62b54156`, SAM_Tas `e84aeff6`, SAM_OpenStudio `d4216f3`,
+SAM_Validation `68eded0`, all `sow/2026-Q4`, clean.
+- `dotnet build WPF/SAM.Analytical.UI.WPF.Tests -c Release`: 0 errors; no new warnings in the changed files.
+- Focused (`SimulationRunProvenanceTests|TasRunManifestTests|TasFailureReportingTests|SimulationConcurrencyGuardTests|EngineComparisonTests|OpenStudioSimulationTests|WpfCollectionTests`): **107/107 PASS** on `87126efc` (105/105 on `29926a2f`), including the real OpenStudio run.
+- Mutation checks on `29926a2f` (each reverted): no Tas record → 6 fail; `SAM_COMMIT` used as source revision → 1 fails;
+ weather "Embedded" by presence instead of content → 3 fail; status ignores an exception → 2 fail.
+- Full `dotnet test WPF/SAM.Analytical.UI.WPF.Tests -c Release` (real `APPDATA`): `29926a2f` **2910/2910 PASS**; after the review fixes `87126efc` **2912/2912 PASS** (2860 + 52 new).
+- Real engines (manifest evidence, not the S05 real-UI acceptance): Tas only explicit and embedded, OpenStudio only explicit and embedded, Both with and without `SAM_COMMIT`; see below.
+
+## Independent review (2026-10-19) and fixes in `87126efc`
+A separate regression-minded review of the combined S03+S04 diff confirmed no S03 status, exception, cancellation or
+output change: the exception filter, definite assignment, overload delegation and Part O gating were checked. Fixes:
+- **Weather `contentSha256`** included the WeatherData's own `Guid`, which is new on every read, so it was not a true
+ content hash. It now leaves that `Guid` out. The S03 evidence manifests were written on `29926a2f`, before this fix, so
+ their `contentSha256` values include the `Guid`. `Embedded` detection was correct either way.
+- **`TBD.Document`** is now looked up in both registry views, so a 32-bit-only registration is no longer reported as "none".
+- **No stale reasons:** a collector that fails part-way now leaves its item `unknown` with no reasons left over for its
+ sub-items. The test helper also checks that every reason names an item that is `unknown`.
+- **Tests that set `SAM_COMMIT`** now run in a non-parallel `ProcessEnvironmentCollection`.
+- **New tests (2), mutation-checked:** removing the stale-reason cleanup → 1 fails; keeping the `Guid` in the content
+ hash → 1 fails.
+- **Accepted, not changed:**
+ - The weather file is hashed when the run ends, not when the dialog read it.
+ - Provenance for the Tas route is collected on the UI thread in the `finally`. The TBD.exe hash is cached per process;
+ the added time was not measured.
+
+## Representative manifest evidence
+`documentation/evidence/s03-20261019/` (README there): six manifests from real engines on this licensed laptop, driven
+through the SAM_UI code paths by a temporary harness (kept as `tools/S03EvidenceHarness.cs.txt`, not compiled), not the
+WPF ribbon. Excerpt of `tas-explicit/box.tas-run.json` (real TAS 9.5.7, Budapest EPW):
+
+```json
+"status": "Succeeded",
+"provenance": {
+ "schema": "sam-ui.run-provenance/1", "route": "Tas",
+ "application": { "sourceRevision": "unknown", "components": [ { "name": "SAM.Analytical", "sha256": "sha256:…" }, … ] },
+ "engine": { "comServer": "TBD.Document", "version": "9.5.7.0",
+ "executable": { "path": "C:\\Program Files\\Environmental Design Solutions Ltd\\Tas\\TBD.exe", "sha256": "sha256:aa658fcd…", "productVersion": "9.5.7.0" } },
+ "weather": { "source": "Explicit", "contentSha256": "sha256:357bc8af…",
+ "file": { "path": "C:\\TasOut\\s01b\\inputs\\Budapest.epw", "sha256": "sha256:472ffb4e…" } },
+ "designDays": { "source": "DerivedFromWeather", "usedForSizing": true, … },
+ "settings": { "workflow": { "SimulateFrom": 1, "SimulateTo": 365, "Sizing": true, "T3DRoute": "Direct", … }, "inputs": { … } },
+ "unknown": { "application.sourceRevision": "this build's InformationalVersion '1.0.0' has no source revision after '+' (a local developer build); …" }
+}
+```
+
+## Findings surfaced by the new provenance (not fixed here)
+- **Both needs `SAM_COMMIT` with developer-built producers (blocker for the roadmap wording, owner decision).** With
+ `SAM_COMMIT` unset, both producers refused their own B1a (`provenance.commit.malformed` at `$.provenance.samCommit`,
+ exit 4) and the run Failed (`both-no-samcommit/`, logs included). This is the producers' existing behaviour, not an S03
+ change; S03 makes it visible (`environment.SAM_COMMIT: "not set"`, `reported: "unknown"` with the reason). SAM_UI cannot
+ supply a verified value. It resolves once the producers are stamped builds (roadmap S06), or by a SAM_Tas /
+ SAM_OpenStudio / SAM_Validation change that is outside S03's scope.
+- **OpenStudio CLI/SDK mismatch on this laptop:** OpenStudio Simulation loads the OpenStudio 3.10.0 SDK (EnergyPlus 25.1.0)
+ in-process, but the CLI discovered on `PATH`, which runs the simulation, is ladybug_tools' OpenStudio 3.11.0, and the
+ EnergyPlus that ran is 25.2.0-cf7368216c (from `eplusout.err`). Earlier records that say "OpenStudio 3.10 / EnergyPlus
+ 25.1" for SAM_UI-launched runs may describe the SDK, not the engine that ran. In the Both evidence, with OpenStudio
+ 3.10.0 first on the producers' `PATH`, benchmark-openstudio reports EnergyPlus 25.1.0. So OpenStudio only and Both can run
+ different EnergyPlus builds on one machine, depending on `PATH`. Roadmap S06 (installed candidate with pinned OpenStudio
+ dependencies) is where this belongs.
+
+## Unresolved, risks
+- **Roadmap acceptance, partly met:** every Tas and OpenStudio manifest has every item without manual environment
+ variables, and every Both manifest records every item it can (unknown with a reason otherwise). But a Both run with
+ developer-built producers does not *complete* without `SAM_COMMIT` (finding above). Owner decision: accept until S06, or
+ a separate producer/SAM_Validation change (for example an explicit unknown `samCommit`).
+- The Part O routes (Hub, guided Part O) write no `tas-run.json`.
+- **Merge with S04 (SAM_UI#243).** Both PRs append assertions to the end of `OpenStudioSimulationTests.RealRun_…`, so
+ whichever PR merges second has a textual conflict there. The resolution is to keep both blocks. It was resolved that
+ way in a local integration merge, which passed the full suite (see the PR description).
+- No licensed real-UI run of the three routes on this content (roadmap S05). See the evidence README for what was run.
+- `sourceRevision` of an installed build is the SAM_Deploy commit (what its installer stamps), not a SAM commit; the record
+ says so in `sourceRevisionOf`.
+
+## Next step
+Owner review of the PR. After merge, add the post-merge `PROJECT_PROGRESS.md` closeout on `sow/2026-Q4`.
diff --git a/documentation/evidence/s03-20261019/README.md b/documentation/evidence/s03-20261019/README.md
new file mode 100644
index 00000000..cc0ec291
--- /dev/null
+++ b/documentation/evidence/s03-20261019/README.md
@@ -0,0 +1,21 @@
+# S03 run provenance - representative manifests (2026-10-19)
+
+Real engines on the licensed development laptop (TAS 9.5.7, OpenStudio SDK 3.10.0), SAM_UI at S03 content `29926a2f`,
+dependencies SAM `62b54156`, SAM_Tas `e84aeff6`, SAM_OpenStudio `d4216f3`, SAM_Validation `68eded0`. Model
+`box-aperture.json` and `Budapest.epw` from `C:\TasOut\s01b\inputs` (machine-local, as in S01b).
+
+**What ran:** the SAM_UI code paths, driven by `tools/S03EvidenceHarness.cs.txt` (a temporary xUnit harness, not part of the
+test project) - not the WPF ribbon. The Tas cases use the ordinary Energy Simulation / Run Simulation > Tas core with the real
+TAS workflow (`Modify.Simulate` seam with no workflow override). This is manifest evidence, **not** the S05 licensed
+real-UI acceptance. Paths in the files are machine-local.
+
+| Folder | Route | Outcome | What it shows |
+|---|---|---|---|
+| `tas-explicit/box.tas-run.json` | Tas only, real TAS, explicit EPW | Succeeded | TAS 9.5.7.0 from the registered `TBD.Document` server with its SHA-256; weather `Explicit` with the EPW's SHA-256 (equal to S01b's `472ffb4e…`) and the WeatherData content hash; design days `DerivedFromWeather`; the `WorkflowSettings` that ran |
+| `tas-embedded/box.tas-run.json` | Tas only, real TAS, model weather | Succeeded | weather `Embedded` by content hash; file unknown with its reason |
+| `openstudio-explicit/openstudio-run.json` | OpenStudio only, explicit EPW | Succeeded | SDK 3.10.0 / EnergyPlus 25.1.0 in-process, but the CLI that ran is ladybug_tools' OpenStudio 3.11.0 and EnergyPlus 25.2.0 (from `eplusout.err`) - see the finding in the PR record |
+| `openstudio-embedded/openstudio-run.json` | OpenStudio only, embedded weather | Succeeded | weather `Embedded`; the exported EPW's hash |
+| `both-no-samcommit/` | Both, real producers, `SAM_COMMIT` not set | Failed | both developer-built producers refuse their own B1a (`provenance.commit.malformed` at `$.provenance.samCommit`, logs included); the manifest still records each producer's binary, `environment.SAM_COMMIT: "not set"` and `reported: "unknown"` with the reason |
+| `both-manual-samcommit/` | Both, real producers, `SAM_COMMIT=62b54156…` | Compared | the producers' reported `samCommit` came from the manual override, which the manifest records under `environment`; its value was **not** verified to be the commit the producers were built from |
+
+For the Both cases the harness put the OpenStudio 3.10.0 `bin` first on the producers' `PATH`, as the S01b runs did.
diff --git a/documentation/evidence/s03-20261019/both-manual-samcommit/engine-comparison-run.json b/documentation/evidence/s03-20261019/both-manual-samcommit/engine-comparison-run.json
new file mode 100644
index 00000000..ca061b08
--- /dev/null
+++ b/documentation/evidence/s03-20261019/both-manual-samcommit/engine-comparison-run.json
@@ -0,0 +1,231 @@
+{
+ "schema": "sam-ui.engine-comparison-run/1",
+ "id": "94bdd02a",
+ "status": "Compared",
+ "startedUtc": "2026-10-19T20:34:32.7455828Z",
+ "finishedUtc": "2026-10-19T20:35:04.1132289Z",
+ "sourcePath": "C:\\TasOut\\s01b\\inputs\\box-aperture.json",
+ "modelName": "000000_SAM_AnalyticalModel",
+ "snapshot": {
+ "path": "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\snapshot.json",
+ "sha256": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05"
+ },
+ "weather": {
+ "source": "File",
+ "path": "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\weather.epw",
+ "sha256": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3"
+ },
+ "producers": [
+ {
+ "engine": "Tas",
+ "executable": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_Tas\\build\\benchmark-tas.exe",
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\weather.epw",
+ "--tbd",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\tas\\000000_SAM_AnalyticalModel.tbd",
+ "--out",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-TAS.json"
+ ],
+ "exitCode": 0,
+ "durationSeconds": 23.514,
+ "log": "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-tas.log",
+ "benchmark": "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-TAS.json",
+ "error": null,
+ "succeeded": true
+ },
+ {
+ "engine": "OpenStudio",
+ "executable": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_OpenStudio\\benchmark\\SAM.Analytical.OpenStudio.Benchmark\\bin\\x64\\Release\\net8.0\\benchmark-openstudio.exe",
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\weather.epw",
+ "--out",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-OpenStudio.json",
+ "--work",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\openstudio"
+ ],
+ "exitCode": 0,
+ "durationSeconds": 7.335,
+ "log": "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-openstudio.log",
+ "benchmark": "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-OpenStudio.json",
+ "error": null,
+ "succeeded": true
+ }
+ ],
+ "gate": "Fail",
+ "reports": [
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\comparison.md",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\comparison.csv",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\comparison-summary.json"
+ ],
+ "diagnostics": [],
+ "error": null,
+ "provenance": {
+ "schema": "sam-ui.run-provenance/1",
+ "route": "Both",
+ "application": {
+ "entryAssembly": {
+ "name": "testhost",
+ "version": "15.0.0.0",
+ "informationalVersion": "17.11.1",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\testhost.dll",
+ "sha256": "sha256:d3817a1f17e00b7f7b1040ab01e7aae73378f80c32aa4f62cb99c2aeed3cd5ad"
+ },
+ "sourceRevision": "unknown",
+ "sourceRevisionOf": "the \u002Bbuild metadata of SAM.Analytical.UI.WPF\u0027s InformationalVersion; SAM_Deploy installers stamp their SAM_Deploy commit, which pins every repository",
+ "components": [
+ {
+ "name": "SAM.Core",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.dll",
+ "sha256": "sha256:7a3c7b15908467342c60cde31b565dc026b47f8a0cd1e3160c14bf163559d8a9"
+ },
+ {
+ "name": "SAM.Analytical",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.dll",
+ "sha256": "sha256:9576dcd158e3545d73f6b29706b73af7d20c09325193257927bc46ee31119970"
+ },
+ {
+ "name": "SAM.Analytical.UI.WPF",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.UI.WPF.dll",
+ "sha256": "sha256:d3827634681ffaf848982bd608c7ef54c087d6491a26f499ff3ff8ae18e69009"
+ },
+ {
+ "name": "SAM.Analytical.Benchmark",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.Benchmark.dll",
+ "sha256": "sha256:5242c29fc8f21dab17129ef7f83a0c890b5fcd65012765c122913685af8d4f83"
+ },
+ {
+ "name": "SAM.Analytical.Benchmark.Compare",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.Benchmark.Compare.dll",
+ "sha256": "sha256:e387fc07802de71731825f0e77f37d9ed2f70301cba8096e82684872291170a9"
+ }
+ ]
+ },
+ "producers": {
+ "Tas": {
+ "executable": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_Tas\\build\\benchmark-tas.exe",
+ "sha256": "sha256:07ab3e45b107bdd28a7d6b3622d05014b1edb95e015f86e5dc3e9c7a9ab3fbd2",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0",
+ "assembly": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_Tas\\build\\benchmark-tas.dll",
+ "sha256": "sha256:7a70ba2d29f4cf49da2f7074fff7aabe356c98ddd3bd4b95fb54cba47ca3c412",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0"
+ }
+ },
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\weather.epw",
+ "--tbd",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\tas\\000000_SAM_AnalyticalModel.tbd",
+ "--out",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-TAS.json"
+ ],
+ "succeeded": true,
+ "reported": {
+ "samCommit": "62b5415697d1a5af143c6b581b7d42fbd6551ca8",
+ "runnerCommit": "e84aeff687033354e3f17891635fdf10d42ad68f",
+ "engine": {
+ "kind": "TAS",
+ "name": "Tas",
+ "version": "9.5.7.0",
+ "sdkVersion": "unknown"
+ },
+ "route": "Native-TAS",
+ "weather": {
+ "identity": "weather",
+ "hash": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3"
+ },
+ "designDaySource": "None",
+ "sourceFileHash": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05",
+ "canonicalModelHash": "sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500",
+ "canonicalizationVersion": "1.0.0",
+ "state": "Success"
+ }
+ },
+ "OpenStudio": {
+ "executable": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_OpenStudio\\benchmark\\SAM.Analytical.OpenStudio.Benchmark\\bin\\x64\\Release\\net8.0\\benchmark-openstudio.exe",
+ "sha256": "sha256:644059197b0638bdbfcf7d41b3a7c375b40198a7f53336d907c3e727619a5e06",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0",
+ "assembly": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_OpenStudio\\benchmark\\SAM.Analytical.OpenStudio.Benchmark\\bin\\x64\\Release\\net8.0\\benchmark-openstudio.dll",
+ "sha256": "sha256:dfc912b3192ec93c23f165761feaa6bea44732e958c359c6f52dea22a173c060",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0"
+ }
+ },
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\weather.epw",
+ "--out",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\benchmark-OpenStudio.json",
+ "--work",
+ "C:\\TasOut\\s03\\both-manual-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203432_94bdd02a\\openstudio"
+ ],
+ "succeeded": true,
+ "reported": {
+ "samCommit": "62b5415697d1a5af143c6b581b7d42fbd6551ca8",
+ "runnerCommit": "d4216f3525c01ca762c1b3e9b6d099733d037f54",
+ "engine": {
+ "kind": "OpenStudio",
+ "name": "EnergyPlus",
+ "version": "25.1.0",
+ "sdkVersion": "3.10.0"
+ },
+ "route": "Native-OpenStudio",
+ "weather": {
+ "identity": "weather",
+ "hash": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3"
+ },
+ "designDaySource": "None",
+ "sourceFileHash": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05",
+ "canonicalModelHash": "sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500",
+ "canonicalizationVersion": "1.0.0",
+ "state": "Success"
+ }
+ }
+ },
+ "environment": {
+ "SAM_COMMIT": "62b5415697d1a5af143c6b581b7d42fbd6551ca8",
+ "RUNNER_COMMIT": "not set"
+ },
+ "weather": {
+ "source": "File",
+ "sha256": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3",
+ "originalPath": "C:\\TasOut\\s01b\\inputs\\Budapest.epw"
+ },
+ "designDays": {
+ "source": "chosen by each producer from the snapshot (SAM_UI passes none): see producers.\u003Cengine\u003E.reported.designDaySource"
+ },
+ "settings": {
+ "producers": "each producer\u0027s arguments (producers.\u003Cengine\u003E.arguments) and its built-in workflow settings, which its executable SHA-256 pins"
+ },
+ "unknown": {
+ "application.sourceRevision": "this build\u0027s InformationalVersion \u00271.0.0\u0027 has no source revision after \u0027\u002B\u0027 (a local developer build); the component SHA-256 values identify the binaries",
+ "producers.Tas.reported.engine.sdkVersion": "the producer reported none (null in its B1a document; its warnings and notes say why)"
+ }
+ }
+}
\ No newline at end of file
diff --git a/documentation/evidence/s03-20261019/both-no-samcommit/engine-comparison-run.json b/documentation/evidence/s03-20261019/both-no-samcommit/engine-comparison-run.json
new file mode 100644
index 00000000..8e166c93
--- /dev/null
+++ b/documentation/evidence/s03-20261019/both-no-samcommit/engine-comparison-run.json
@@ -0,0 +1,192 @@
+{
+ "schema": "sam-ui.engine-comparison-run/1",
+ "id": "23f94fe4",
+ "status": "Failed",
+ "startedUtc": "2026-10-19T20:35:04.2543222Z",
+ "finishedUtc": "2026-10-19T20:35:32.8855125Z",
+ "sourcePath": "C:\\TasOut\\s01b\\inputs\\box-aperture.json",
+ "modelName": "000000_SAM_AnalyticalModel",
+ "snapshot": {
+ "path": "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\snapshot.json",
+ "sha256": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05"
+ },
+ "weather": {
+ "source": "File",
+ "path": "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\weather.epw",
+ "sha256": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3"
+ },
+ "producers": [
+ {
+ "engine": "Tas",
+ "executable": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_Tas\\build\\benchmark-tas.exe",
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\weather.epw",
+ "--tbd",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\tas\\000000_SAM_AnalyticalModel.tbd",
+ "--out",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\benchmark-TAS.json"
+ ],
+ "exitCode": 4,
+ "durationSeconds": 20.882,
+ "log": "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\benchmark-tas.log",
+ "benchmark": null,
+ "error": "The producer exited with code 4 (2 usage, 3 input/IO, 4 validation, 5 producer failure); see benchmark-tas.log",
+ "succeeded": false
+ },
+ {
+ "engine": "OpenStudio",
+ "executable": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_OpenStudio\\benchmark\\SAM.Analytical.OpenStudio.Benchmark\\bin\\x64\\Release\\net8.0\\benchmark-openstudio.exe",
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\weather.epw",
+ "--out",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\benchmark-OpenStudio.json",
+ "--work",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\openstudio"
+ ],
+ "exitCode": 4,
+ "durationSeconds": 7.299,
+ "log": "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\benchmark-openstudio.log",
+ "benchmark": null,
+ "error": "The producer exited with code 4 (2 usage, 3 input/IO, 4 validation, 5 producer failure); see benchmark-openstudio.log",
+ "succeeded": false
+ }
+ ],
+ "gate": null,
+ "reports": [],
+ "diagnostics": [
+ "Error: no comparison - neither Tas nor OpenStudio completed"
+ ],
+ "error": null,
+ "provenance": {
+ "schema": "sam-ui.run-provenance/1",
+ "route": "Both",
+ "application": {
+ "entryAssembly": {
+ "name": "testhost",
+ "version": "15.0.0.0",
+ "informationalVersion": "17.11.1",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\testhost.dll",
+ "sha256": "sha256:d3817a1f17e00b7f7b1040ab01e7aae73378f80c32aa4f62cb99c2aeed3cd5ad"
+ },
+ "sourceRevision": "unknown",
+ "sourceRevisionOf": "the \u002Bbuild metadata of SAM.Analytical.UI.WPF\u0027s InformationalVersion; SAM_Deploy installers stamp their SAM_Deploy commit, which pins every repository",
+ "components": [
+ {
+ "name": "SAM.Core",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.dll",
+ "sha256": "sha256:7a3c7b15908467342c60cde31b565dc026b47f8a0cd1e3160c14bf163559d8a9"
+ },
+ {
+ "name": "SAM.Analytical",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.dll",
+ "sha256": "sha256:9576dcd158e3545d73f6b29706b73af7d20c09325193257927bc46ee31119970"
+ },
+ {
+ "name": "SAM.Analytical.UI.WPF",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.UI.WPF.dll",
+ "sha256": "sha256:d3827634681ffaf848982bd608c7ef54c087d6491a26f499ff3ff8ae18e69009"
+ },
+ {
+ "name": "SAM.Analytical.Benchmark",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.Benchmark.dll",
+ "sha256": "sha256:5242c29fc8f21dab17129ef7f83a0c890b5fcd65012765c122913685af8d4f83"
+ },
+ {
+ "name": "SAM.Analytical.Benchmark.Compare",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.Benchmark.Compare.dll",
+ "sha256": "sha256:e387fc07802de71731825f0e77f37d9ed2f70301cba8096e82684872291170a9"
+ }
+ ]
+ },
+ "producers": {
+ "Tas": {
+ "executable": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_Tas\\build\\benchmark-tas.exe",
+ "sha256": "sha256:07ab3e45b107bdd28a7d6b3622d05014b1edb95e015f86e5dc3e9c7a9ab3fbd2",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0",
+ "assembly": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_Tas\\build\\benchmark-tas.dll",
+ "sha256": "sha256:7a70ba2d29f4cf49da2f7074fff7aabe356c98ddd3bd4b95fb54cba47ca3c412",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0"
+ }
+ },
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\weather.epw",
+ "--tbd",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\tas\\000000_SAM_AnalyticalModel.tbd",
+ "--out",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\benchmark-TAS.json"
+ ],
+ "succeeded": false,
+ "reported": "unknown"
+ },
+ "OpenStudio": {
+ "executable": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_OpenStudio\\benchmark\\SAM.Analytical.OpenStudio.Benchmark\\bin\\x64\\Release\\net8.0\\benchmark-openstudio.exe",
+ "sha256": "sha256:644059197b0638bdbfcf7d41b3a7c375b40198a7f53336d907c3e727619a5e06",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0",
+ "assembly": {
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_OpenStudio\\benchmark\\SAM.Analytical.OpenStudio.Benchmark\\bin\\x64\\Release\\net8.0\\benchmark-openstudio.dll",
+ "sha256": "sha256:dfc912b3192ec93c23f165761feaa6bea44732e958c359c6f52dea22a173c060",
+ "fileVersion": "1.0.0.0",
+ "productVersion": "1.0.0"
+ }
+ },
+ "arguments": [
+ "--model",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\snapshot.json",
+ "--weather",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\weather.epw",
+ "--out",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\benchmark-OpenStudio.json",
+ "--work",
+ "C:\\TasOut\\s03\\both-no-samcommit\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-203504_23f94fe4\\openstudio"
+ ],
+ "succeeded": false,
+ "reported": "unknown"
+ }
+ },
+ "environment": {
+ "SAM_COMMIT": "not set",
+ "RUNNER_COMMIT": "not set"
+ },
+ "weather": {
+ "source": "File",
+ "sha256": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3",
+ "originalPath": "C:\\TasOut\\s01b\\inputs\\Budapest.epw"
+ },
+ "designDays": {
+ "source": "chosen by each producer from the snapshot (SAM_UI passes none): see producers.\u003Cengine\u003E.reported.designDaySource"
+ },
+ "settings": {
+ "producers": "each producer\u0027s arguments (producers.\u003Cengine\u003E.arguments) and its built-in workflow settings, which its executable SHA-256 pins"
+ },
+ "unknown": {
+ "application.sourceRevision": "this build\u0027s InformationalVersion \u00271.0.0\u0027 has no source revision after \u0027\u002B\u0027 (a local developer build); the component SHA-256 values identify the binaries",
+ "producers.Tas.reported": "the producer wrote no B1a document",
+ "producers.OpenStudio.reported": "the producer wrote no B1a document"
+ }
+ }
+}
\ No newline at end of file
diff --git a/documentation/evidence/s03-20261019/openstudio-embedded/openstudio-run.json b/documentation/evidence/s03-20261019/openstudio-embedded/openstudio-run.json
new file mode 100644
index 00000000..154cf1e2
--- /dev/null
+++ b/documentation/evidence/s03-20261019/openstudio-embedded/openstudio-run.json
@@ -0,0 +1,158 @@
+{
+ "schema": "sam-ui.openstudio-run/1",
+ "id": "2af39d1d",
+ "status": "Succeeded",
+ "startedUtc": "2026-10-19T20:37:14.7659527Z",
+ "finishedUtc": "2026-10-19T20:37:24.8054232Z",
+ "durationSeconds": 10.039,
+ "sourcePath": "C:\\TasOut\\s01b\\inputs\\box-aperture.json",
+ "model": {
+ "name": "000000_SAM_AnalyticalModel",
+ "guid": "1988f7b93ec047d0b87bf9cb345d9eab"
+ },
+ "snapshot": {
+ "path": "snapshot.json",
+ "sha256": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05"
+ },
+ "weather": {
+ "source": "Embedded",
+ "path": "weather.epw",
+ "sha256": "sha256:e1353bd2600a401f76262d52a8e16176a15dc45a4967e58d9107c374704abf7c"
+ },
+ "engine": {
+ "route": "Native-OpenStudio",
+ "samOpenStudioVersion": "1.0.0.0",
+ "cliPath": "C:\\Program Files\\ladybug_tools\\openstudio\\bin\\openstudio.exe",
+ "exitCode": 0,
+ "warningCount": 10,
+ "severeCount": 0,
+ "fatalCount": 0
+ },
+ "outputs": {
+ "osm": "000000_SAM_AnalyticalModel.osm",
+ "osw": "000000_SAM_AnalyticalModel.osw",
+ "sql": "run/eplusout.sql",
+ "err": "run/eplusout.err",
+ "resultModel": "000000_SAM_AnalyticalModel_OpenStudio.json"
+ },
+ "resultCount": 13,
+ "removedResultCount": 0,
+ "diagnostics": [
+ "Warning SAM-OS-IC-001: Pollutant generation is not converted (EnergyPlus generic contaminant modelling is out of scope)",
+ "Information SAM-OS-HVAC-001: Exhaust air flows (ExhaustAirFlowPerPerson) are not converted \u2014 exhaust needs fan/air-loop equipment, deferred to the detailed-HVAC programme",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (1) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (5.565) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (2.7) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (24) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (2) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (1) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (1) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-RUN-002: Annual weather source: AnalyticalModel WeatherData (exported to an EPW file)",
+ "Information SAM-OS-RUN-001: Site coordinates taken from the SAM model Location (lat 47.517, lon 19.033, elevation 129 m), overriding the EPW header; the time zone stays with the EPW header",
+ "Information SAM-OS-RUN-002: Ground temperatures taken from the SAM model WeatherData (depth 0.5 m set, 12 monthly values)",
+ "Information SAM-OS-RUN-002: No design-day source supplied; sizing-period runs stay disabled unless OpenStudioConversionOptions.RunSizingPeriods overrides"
+ ],
+ "resultDiagnostics": [],
+ "error": null,
+ "provenance": {
+ "schema": "sam-ui.run-provenance/1",
+ "route": "OpenStudio",
+ "application": {
+ "entryAssembly": {
+ "name": "testhost",
+ "version": "15.0.0.0",
+ "informationalVersion": "17.11.1",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\testhost.dll",
+ "sha256": "sha256:d3817a1f17e00b7f7b1040ab01e7aae73378f80c32aa4f62cb99c2aeed3cd5ad"
+ },
+ "sourceRevision": "unknown",
+ "sourceRevisionOf": "the \u002Bbuild metadata of SAM.Analytical.UI.WPF\u0027s InformationalVersion; SAM_Deploy installers stamp their SAM_Deploy commit, which pins every repository",
+ "components": [
+ {
+ "name": "SAM.Core",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.dll",
+ "sha256": "sha256:7a3c7b15908467342c60cde31b565dc026b47f8a0cd1e3160c14bf163559d8a9"
+ },
+ {
+ "name": "SAM.Analytical",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.dll",
+ "sha256": "sha256:9576dcd158e3545d73f6b29706b73af7d20c09325193257927bc46ee31119970"
+ },
+ {
+ "name": "SAM.Analytical.UI.WPF",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.UI.WPF.dll",
+ "sha256": "sha256:d3827634681ffaf848982bd608c7ef54c087d6491a26f499ff3ff8ae18e69009"
+ },
+ {
+ "name": "SAM.Analytical.OpenStudio",
+ "version": "1.0.0.0",
+ "informationalVersion": "unknown",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.OpenStudio.dll",
+ "sha256": "sha256:b1b5b9efce7ebb0938b8dda3bf8bc497091ee79083dcd53f446edd65e97402bb"
+ },
+ {
+ "name": "SAM.Core.OpenStudio",
+ "version": "1.0.0.0",
+ "informationalVersion": "unknown",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.OpenStudio.dll",
+ "sha256": "sha256:09c77b85b9a54d192c23878e5956e9b41a7c4e9db5ed37030591e9a51247a842"
+ }
+ ]
+ },
+ "engine": {
+ "openStudioSdkVersion": "3.10.0",
+ "energyPlusVersionOfSdk": "25.1.0",
+ "energyPlusVersion": "25.2.0-cf7368216c",
+ "cli": {
+ "path": "C:\\Program Files\\ladybug_tools\\openstudio\\bin\\openstudio.exe",
+ "sha256": "sha256:ef290e843774448c18467debb286abc90dc8625c0679c011cb4892e519100b6e",
+ "fileVersion": "3.11.0",
+ "productVersion": "3.11.0.241b8abb4d"
+ }
+ },
+ "weather": {
+ "source": "Embedded",
+ "sha256": "sha256:e1353bd2600a401f76262d52a8e16176a15dc45a4967e58d9107c374704abf7c",
+ "originalPath": "unknown"
+ },
+ "designDays": {
+ "source": "None"
+ },
+ "settings": {
+ "runOptions": {
+ "epwPath": "(none: the model\u0027s embedded WeatherData, SAM_OpenStudio\u0027s own precedence)",
+ "outputDirectory": "C:\\TasOut\\s03\\openstudio-embedded\\000000_SAM_AnalyticalModel_OpenStudio_20261019-203714_2af39d1d",
+ "conversionOptions": "SAM_OpenStudio defaults",
+ "run": true,
+ "cliPath": "(discovered by SAM_OpenStudio; the CLI that ran is engine.cli)",
+ "runDirectory": "(not set: a directory next to the OSW file)",
+ "ddyPath": "(none)",
+ "timeoutSeconds": 3600,
+ "useUniqueRunDirectory": false,
+ "extractTimeSeries": false
+ }
+ },
+ "unknown": {
+ "application.sourceRevision": "this build\u0027s InformationalVersion \u00271.0.0\u0027 has no source revision after \u0027\u002B\u0027 (a local developer build); the component SHA-256 values identify the binaries",
+ "application.components[SAM.Analytical.OpenStudio].informationalVersion": "the assembly carries no InformationalVersion",
+ "application.components[SAM.Core.OpenStudio].informationalVersion": "the assembly carries no InformationalVersion",
+ "weather.originalPath": "the weather is the model\u0027s embedded WeatherData, exported to EPW by SAM_OpenStudio; sha256 identifies the exported file"
+ }
+ }
+}
\ No newline at end of file
diff --git a/documentation/evidence/s03-20261019/openstudio-explicit/openstudio-run.json b/documentation/evidence/s03-20261019/openstudio-explicit/openstudio-run.json
new file mode 100644
index 00000000..872a2702
--- /dev/null
+++ b/documentation/evidence/s03-20261019/openstudio-explicit/openstudio-run.json
@@ -0,0 +1,157 @@
+{
+ "schema": "sam-ui.openstudio-run/1",
+ "id": "a3e1abb2",
+ "status": "Succeeded",
+ "startedUtc": "2026-10-19T20:37:24.9571192Z",
+ "finishedUtc": "2026-10-19T20:37:33.9366947Z",
+ "durationSeconds": 8.98,
+ "sourcePath": "C:\\TasOut\\s01b\\inputs\\box-aperture.json",
+ "model": {
+ "name": "000000_SAM_AnalyticalModel",
+ "guid": "1988f7b93ec047d0b87bf9cb345d9eab"
+ },
+ "snapshot": {
+ "path": "snapshot.json",
+ "sha256": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05"
+ },
+ "weather": {
+ "source": "File",
+ "path": "weather.epw",
+ "sha256": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3"
+ },
+ "engine": {
+ "route": "Native-OpenStudio",
+ "samOpenStudioVersion": "1.0.0.0",
+ "cliPath": "C:\\Program Files\\ladybug_tools\\openstudio\\bin\\openstudio.exe",
+ "exitCode": 0,
+ "warningCount": 10,
+ "severeCount": 0,
+ "fatalCount": 0
+ },
+ "outputs": {
+ "osm": "000000_SAM_AnalyticalModel.osm",
+ "osw": "000000_SAM_AnalyticalModel.osw",
+ "sql": "run/eplusout.sql",
+ "err": "run/eplusout.err",
+ "resultModel": "000000_SAM_AnalyticalModel_OpenStudio.json"
+ },
+ "resultCount": 13,
+ "removedResultCount": 0,
+ "diagnostics": [
+ "Warning SAM-OS-IC-001: Pollutant generation is not converted (EnergyPlus generic contaminant modelling is out of scope)",
+ "Information SAM-OS-HVAC-001: Exhaust air flows (ExhaustAirFlowPerPerson) are not converted \u2014 exhaust needs fan/air-loop equipment, deferred to the detailed-HVAC programme",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (1) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (5.565) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (2.7) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (24) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (2) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (1) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (1) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-MAT-002: Vapour diffusion factor (9999) is not converted \u2014 EnergyPlus moisture modelling (HAMT/EMPD) needs data SAM does not carry",
+ "Information SAM-OS-RUN-002: Annual weather source: explicit EPW (Budapest.epw)",
+ "Information SAM-OS-RUN-001: Site coordinates taken from the SAM model Location (lat 47.517, lon 19.033, elevation 129 m), overriding the EPW header; the time zone stays with the EPW header",
+ "Information SAM-OS-RUN-002: Ground temperatures taken from the SAM model WeatherData (depth 0.5 m set, 12 monthly values)",
+ "Information SAM-OS-RUN-002: No design-day source supplied; sizing-period runs stay disabled unless OpenStudioConversionOptions.RunSizingPeriods overrides"
+ ],
+ "resultDiagnostics": [],
+ "error": null,
+ "provenance": {
+ "schema": "sam-ui.run-provenance/1",
+ "route": "OpenStudio",
+ "application": {
+ "entryAssembly": {
+ "name": "testhost",
+ "version": "15.0.0.0",
+ "informationalVersion": "17.11.1",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\testhost.dll",
+ "sha256": "sha256:d3817a1f17e00b7f7b1040ab01e7aae73378f80c32aa4f62cb99c2aeed3cd5ad"
+ },
+ "sourceRevision": "unknown",
+ "sourceRevisionOf": "the \u002Bbuild metadata of SAM.Analytical.UI.WPF\u0027s InformationalVersion; SAM_Deploy installers stamp their SAM_Deploy commit, which pins every repository",
+ "components": [
+ {
+ "name": "SAM.Core",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.dll",
+ "sha256": "sha256:7a3c7b15908467342c60cde31b565dc026b47f8a0cd1e3160c14bf163559d8a9"
+ },
+ {
+ "name": "SAM.Analytical",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.dll",
+ "sha256": "sha256:9576dcd158e3545d73f6b29706b73af7d20c09325193257927bc46ee31119970"
+ },
+ {
+ "name": "SAM.Analytical.UI.WPF",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.UI.WPF.dll",
+ "sha256": "sha256:d3827634681ffaf848982bd608c7ef54c087d6491a26f499ff3ff8ae18e69009"
+ },
+ {
+ "name": "SAM.Analytical.OpenStudio",
+ "version": "1.0.0.0",
+ "informationalVersion": "unknown",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.OpenStudio.dll",
+ "sha256": "sha256:b1b5b9efce7ebb0938b8dda3bf8bc497091ee79083dcd53f446edd65e97402bb"
+ },
+ {
+ "name": "SAM.Core.OpenStudio",
+ "version": "1.0.0.0",
+ "informationalVersion": "unknown",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.OpenStudio.dll",
+ "sha256": "sha256:09c77b85b9a54d192c23878e5956e9b41a7c4e9db5ed37030591e9a51247a842"
+ }
+ ]
+ },
+ "engine": {
+ "openStudioSdkVersion": "3.10.0",
+ "energyPlusVersionOfSdk": "25.1.0",
+ "energyPlusVersion": "25.2.0-cf7368216c",
+ "cli": {
+ "path": "C:\\Program Files\\ladybug_tools\\openstudio\\bin\\openstudio.exe",
+ "sha256": "sha256:ef290e843774448c18467debb286abc90dc8625c0679c011cb4892e519100b6e",
+ "fileVersion": "3.11.0",
+ "productVersion": "3.11.0.241b8abb4d"
+ }
+ },
+ "weather": {
+ "source": "File",
+ "sha256": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3",
+ "originalPath": "C:\\TasOut\\s01b\\inputs\\Budapest.epw"
+ },
+ "designDays": {
+ "source": "None"
+ },
+ "settings": {
+ "runOptions": {
+ "epwPath": "C:\\TasOut\\s01b\\inputs\\Budapest.epw",
+ "outputDirectory": "C:\\TasOut\\s03\\openstudio-explicit\\000000_SAM_AnalyticalModel_OpenStudio_20261019-203724_a3e1abb2",
+ "conversionOptions": "SAM_OpenStudio defaults",
+ "run": true,
+ "cliPath": "(discovered by SAM_OpenStudio; the CLI that ran is engine.cli)",
+ "runDirectory": "(not set: a directory next to the OSW file)",
+ "ddyPath": "(none)",
+ "timeoutSeconds": 3600,
+ "useUniqueRunDirectory": false,
+ "extractTimeSeries": false
+ }
+ },
+ "unknown": {
+ "application.sourceRevision": "this build\u0027s InformationalVersion \u00271.0.0\u0027 has no source revision after \u0027\u002B\u0027 (a local developer build); the component SHA-256 values identify the binaries",
+ "application.components[SAM.Analytical.OpenStudio].informationalVersion": "the assembly carries no InformationalVersion",
+ "application.components[SAM.Core.OpenStudio].informationalVersion": "the assembly carries no InformationalVersion"
+ }
+ }
+}
\ No newline at end of file
diff --git a/documentation/evidence/s03-20261019/tas-embedded/box.tas-run.json b/documentation/evidence/s03-20261019/tas-embedded/box.tas-run.json
new file mode 100644
index 00000000..ccaae400
--- /dev/null
+++ b/documentation/evidence/s03-20261019/tas-embedded/box.tas-run.json
@@ -0,0 +1,701 @@
+{
+ "schema": "sam-ui.tas-run/1",
+ "id": "141f0788",
+ "status": "Succeeded",
+ "startedUtc": "2026-10-19T20:35:47.6017256Z",
+ "finishedUtc": "2026-10-19T20:36:01.2691357Z",
+ "durationSeconds": 13.667,
+ "sourcePath": "C:\\TasOut\\s03\\tas-embedded\\box-aperture.json",
+ "projectName": "box",
+ "model": {
+ "name": "000000_SAM_AnalyticalModel",
+ "guid": "1988f7b93ec047d0b87bf9cb345d9eab"
+ },
+ "outputs": {
+ "directory": "C:\\TasOut\\s03\\tas-embedded",
+ "tbd": "C:\\TasOut\\s03\\tas-embedded\\box.tbd",
+ "tsd": "C:\\TasOut\\s03\\tas-embedded\\box.tsd"
+ },
+ "refusal": null,
+ "failure": null,
+ "notes": [],
+ "error": null,
+ "provenance": {
+ "schema": "sam-ui.run-provenance/1",
+ "route": "Tas",
+ "application": {
+ "entryAssembly": {
+ "name": "testhost",
+ "version": "15.0.0.0",
+ "informationalVersion": "17.11.1",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\testhost.dll",
+ "sha256": "sha256:d3817a1f17e00b7f7b1040ab01e7aae73378f80c32aa4f62cb99c2aeed3cd5ad"
+ },
+ "sourceRevision": "unknown",
+ "sourceRevisionOf": "the \u002Bbuild metadata of SAM.Analytical.UI.WPF\u0027s InformationalVersion; SAM_Deploy installers stamp their SAM_Deploy commit, which pins every repository",
+ "components": [
+ {
+ "name": "SAM.Core",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.dll",
+ "sha256": "sha256:7a3c7b15908467342c60cde31b565dc026b47f8a0cd1e3160c14bf163559d8a9"
+ },
+ {
+ "name": "SAM.Analytical",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.dll",
+ "sha256": "sha256:9576dcd158e3545d73f6b29706b73af7d20c09325193257927bc46ee31119970"
+ },
+ {
+ "name": "SAM.Analytical.UI.WPF",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.UI.WPF.dll",
+ "sha256": "sha256:d3827634681ffaf848982bd608c7ef54c087d6491a26f499ff3ff8ae18e69009"
+ },
+ {
+ "name": "SAM.Analytical.Tas",
+ "version": "1.0.0.0",
+ "informationalVersion": "unknown",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.Tas.dll",
+ "sha256": "sha256:84fb3e619305760131318c4a2dbaede84133abbe4d21fd08f9ab8a4fb4d91300"
+ }
+ ]
+ },
+ "engine": {
+ "comServer": "TBD.Document",
+ "executable": {
+ "path": "C:\\Program Files\\Environmental Design Solutions Ltd\\Tas\\TBD.exe",
+ "sha256": "sha256:aa658fcdeec8f5e3acbd64523376b5a7362c499beecc280715edca64851e49ae",
+ "fileVersion": "2, 0, 0, 1",
+ "productVersion": "9.5.7.0"
+ },
+ "version": "9.5.7.0"
+ },
+ "weather": {
+ "source": "Embedded",
+ "name": "HUN_CEN_Budapest.Met.Center, 128400, SRC-TMYx",
+ "contentSha256": "sha256:f7566cb0308ff1c25b142d15577350ab4dc574b77e562b3c8503e96b9e77e723",
+ "contentHashOf": "the SAM WeatherData JSON given to TAS, canonical JSON v1 (SAM_Validation)",
+ "file": "unknown"
+ },
+ "designDays": {
+ "source": "DerivedFromWeather",
+ "heating": [
+ "Budapest.Met.Center ANN HTG 100% CONDS DB"
+ ],
+ "cooling": [
+ "Budapest.Met.Center ANN CLG 0% CONDS DB=\u003EGRad"
+ ],
+ "usedForSizing": true
+ },
+ "settings": {
+ "inputs": {
+ "simulate": true,
+ "fullYearSimulation": true,
+ "fullYearSimulationFrom": 1,
+ "fullYearSimulationTo": 365,
+ "sizing": true,
+ "unmetHours": true,
+ "useWidths": false,
+ "solarCalculationMethod": "TAS",
+ "directT3D": true,
+ "updateConstructionLayersByPanelType": false,
+ "createTM59": false,
+ "createSAP": false,
+ "createTPD": false,
+ "createPartL": false,
+ "printRoomDataSheets": false,
+ "zoneCategory": "(none)"
+ },
+ "workflow": {
+ "_type": "SAM.Analytical.Tas.WorkflowSettings,SAM.Analytical.Tas",
+ "Path_TBD": "C:\\TasOut\\s03\\tas-embedded\\box.tbd",
+ "T3DRoute": "Direct",
+ "WeatherData": {
+ "name": "HUN_CEN_Budapest.Met.Center, 128400, SRC-TMYx",
+ "contentSha256": "sha256:f7566cb0308ff1c25b142d15577350ab4dc574b77e562b3c8503e96b9e77e723"
+ },
+ "DesignDays_Heating": [
+ {
+ "_type": "SAM.Analytical.DesignDay,SAM.Analytical",
+ "Data": [
+ {
+ "Name": "DryBulbTemperature",
+ "Values": [
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9
+ ]
+ },
+ {
+ "Name": "WetBulbTemperature",
+ "Values": [
+ -16.3,
+ -16.5,
+ -16.6,
+ -16.3,
+ -15.4,
+ -15.2,
+ -16.1,
+ -16.2,
+ -15.9,
+ -14.5,
+ -15.2,
+ -13.5,
+ -14.4,
+ -14.5,
+ -14.5,
+ -15,
+ -15.8,
+ -15.1,
+ -14.1,
+ -13.8,
+ -13.8,
+ -13,
+ -12.8,
+ -14.6
+ ]
+ },
+ {
+ "Name": "RelativeHumidity",
+ "Values": [
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65
+ ]
+ },
+ {
+ "Name": "AtmosphericPressure",
+ "Values": [
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310
+ ]
+ },
+ {
+ "Name": "GlobalSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "DiffuseSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "WindDirection",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "WindSpeed",
+ "Values": [
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4
+ ]
+ },
+ {
+ "Name": "CloudCover",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "Name": "Budapest.Met.Center ANN HTG 100% CONDS DB",
+ "Description": "Heating Design Day automated :) \n *calculated from weather data as day with min temp - single value and its wind speed all remaining values are zero",
+ "Year": 2018,
+ "Month": 2,
+ "Day": 8
+ }
+ ],
+ "DesignDays_Cooling": [
+ {
+ "_type": "SAM.Analytical.DesignDay,SAM.Analytical",
+ "Data": [
+ {
+ "Name": "DryBulbTemperature",
+ "Values": [
+ 21.1,
+ 21.7,
+ 22.4,
+ 22.6,
+ 23.1,
+ 23.8,
+ 22.1,
+ 23.9,
+ 25.6,
+ 27.2,
+ 28.9,
+ 30.6,
+ 32.2,
+ 33.9,
+ 33.2,
+ 32.6,
+ 32,
+ 31.3,
+ 24.4,
+ 23.3,
+ 22.8,
+ 22.9,
+ 23,
+ 23.1
+ ]
+ },
+ {
+ "Name": "WetBulbTemperature",
+ "Values": [
+ 19.1,
+ 19.6,
+ 20,
+ 20.7,
+ 20.4,
+ 20.1,
+ 21.9,
+ 19.4,
+ 18.9,
+ 18.3,
+ 17.8,
+ 17.2,
+ 16.6,
+ 16.1,
+ 15.7,
+ 15.1,
+ 14.6,
+ 14,
+ 24.2,
+ 23.2,
+ 21.7,
+ 20.7,
+ 19.5,
+ 18.7
+ ]
+ },
+ {
+ "Name": "RelativeHumidity",
+ "Values": [
+ 89,
+ 88,
+ 86,
+ 89,
+ 85,
+ 80,
+ 100,
+ 76,
+ 66,
+ 58,
+ 51,
+ 45,
+ 39,
+ 35,
+ 35,
+ 35,
+ 35,
+ 35,
+ 99,
+ 99,
+ 93,
+ 87,
+ 80,
+ 76
+ ]
+ },
+ {
+ "Name": "AtmosphericPressure",
+ "Values": [
+ 99788,
+ 99758,
+ 99718,
+ 99733,
+ 99677,
+ 99631,
+ 99825,
+ 99544,
+ 99514,
+ 99484,
+ 99455,
+ 99425,
+ 99395,
+ 99365,
+ 99333,
+ 99300,
+ 99267,
+ 99235,
+ 99709,
+ 99788,
+ 99829,
+ 99830,
+ 99830,
+ 99831
+ ]
+ },
+ {
+ "Name": "GlobalSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 22,
+ 101,
+ 141,
+ 251,
+ 523,
+ 739,
+ 851,
+ 903,
+ 896,
+ 835,
+ 680,
+ 501,
+ 357,
+ 208,
+ 71,
+ 4,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "DiffuseSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 22,
+ 83,
+ 123,
+ 200,
+ 259,
+ 205,
+ 154,
+ 177,
+ 211,
+ 193,
+ 230,
+ 243,
+ 179,
+ 120,
+ 49,
+ 4,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "WindDirection",
+ "Values": [
+ 140,
+ 138,
+ 159,
+ 198,
+ 177,
+ 195,
+ 168,
+ 84,
+ 263,
+ 204,
+ 23,
+ 77,
+ 315,
+ 164,
+ 277,
+ 95,
+ 27,
+ 292,
+ 97,
+ 160,
+ 79,
+ 147,
+ 74,
+ 40
+ ]
+ },
+ {
+ "Name": "WindSpeed",
+ "Values": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.8,
+ 0.5,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0.8,
+ 1.5,
+ 2.3,
+ 3.1,
+ 3.8,
+ 4.6,
+ 4.6,
+ 4.6,
+ 4.6,
+ 4.6
+ ]
+ },
+ {
+ "Name": "CloudCover",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "Name": "Budapest.Met.Center ANN CLG 0% CONDS DB=\u003EGRad",
+ "Description": "Cooling Design Day automated :) \n *calculated from weather data as day with max temp and then radation replaced from max global radation day and cloud cover removed",
+ "Year": 2018,
+ "Month": 7,
+ "Day": 1
+ }
+ ],
+ "SurfaceOutputSpecs": [
+ {
+ "_type": "SAM.Core.Tas.SurfaceOutputSpec,SAM.Core.Tas",
+ "Name": "Tas.Simulate",
+ "Guid": "d3132ea5-5143-4003-9aa2-2468032ece87",
+ "ApertureData": true,
+ "Condensation": false,
+ "Convection": false,
+ "SolarGain": true,
+ "Conduction": true,
+ "LongWave": false,
+ "Temperature": true
+ }
+ ],
+ "UnmetHours": true,
+ "Simulate": true,
+ "Sizing": true,
+ "UpdateZones": true,
+ "UseWidths": false,
+ "AddIZAMs": true,
+ "RemoveIZAMs": false,
+ "RemoveMechanicalVentilationGains": false,
+ "SimulateFrom": 1,
+ "SimulateTo": 365,
+ "RemoveExistingTBD": false,
+ "UpdateWindowPositionType": false
+ }
+ },
+ "unknown": {
+ "application.sourceRevision": "this build\u0027s InformationalVersion \u00271.0.0\u0027 has no source revision after \u0027\u002B\u0027 (a local developer build); the component SHA-256 values identify the binaries",
+ "application.components[SAM.Analytical.Tas].informationalVersion": "the assembly carries no InformationalVersion",
+ "weather.file": "the weather is the model\u0027s embedded WeatherData, not a file; contentSha256 identifies it"
+ }
+ }
+}
\ No newline at end of file
diff --git a/documentation/evidence/s03-20261019/tas-explicit/box.tas-run.json b/documentation/evidence/s03-20261019/tas-explicit/box.tas-run.json
new file mode 100644
index 00000000..39e261fe
--- /dev/null
+++ b/documentation/evidence/s03-20261019/tas-explicit/box.tas-run.json
@@ -0,0 +1,703 @@
+{
+ "schema": "sam-ui.tas-run/1",
+ "id": "faee6224",
+ "status": "Succeeded",
+ "startedUtc": "2026-10-19T20:35:33.0767252Z",
+ "finishedUtc": "2026-10-19T20:35:47.1175414Z",
+ "durationSeconds": 14.041,
+ "sourcePath": "C:\\TasOut\\s03\\tas-explicit\\box-aperture.json",
+ "projectName": "box",
+ "model": {
+ "name": "000000_SAM_AnalyticalModel",
+ "guid": "1988f7b93ec047d0b87bf9cb345d9eab"
+ },
+ "outputs": {
+ "directory": "C:\\TasOut\\s03\\tas-explicit",
+ "tbd": "C:\\TasOut\\s03\\tas-explicit\\box.tbd",
+ "tsd": "C:\\TasOut\\s03\\tas-explicit\\box.tsd"
+ },
+ "refusal": null,
+ "failure": null,
+ "notes": [],
+ "error": null,
+ "provenance": {
+ "schema": "sam-ui.run-provenance/1",
+ "route": "Tas",
+ "application": {
+ "entryAssembly": {
+ "name": "testhost",
+ "version": "15.0.0.0",
+ "informationalVersion": "17.11.1",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\testhost.dll",
+ "sha256": "sha256:d3817a1f17e00b7f7b1040ab01e7aae73378f80c32aa4f62cb99c2aeed3cd5ad"
+ },
+ "sourceRevision": "unknown",
+ "sourceRevisionOf": "the \u002Bbuild metadata of SAM.Analytical.UI.WPF\u0027s InformationalVersion; SAM_Deploy installers stamp their SAM_Deploy commit, which pins every repository",
+ "components": [
+ {
+ "name": "SAM.Core",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Core.dll",
+ "sha256": "sha256:7a3c7b15908467342c60cde31b565dc026b47f8a0cd1e3160c14bf163559d8a9"
+ },
+ {
+ "name": "SAM.Analytical",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.dll",
+ "sha256": "sha256:9576dcd158e3545d73f6b29706b73af7d20c09325193257927bc46ee31119970"
+ },
+ {
+ "name": "SAM.Analytical.UI.WPF",
+ "version": "1.0.0.0",
+ "informationalVersion": "1.0.0",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.UI.WPF.dll",
+ "sha256": "sha256:d3827634681ffaf848982bd608c7ef54c087d6491a26f499ff3ff8ae18e69009"
+ },
+ {
+ "name": "SAM.Analytical.Tas",
+ "version": "1.0.0.0",
+ "informationalVersion": "unknown",
+ "path": "C:\\Users\\michal\\Documents\\GitHub\\SAM-BIM\\SAM_UI\\WPF\\SAM.Analytical.UI.WPF.Tests\\bin\\Release\\SAM.Analytical.Tas.dll",
+ "sha256": "sha256:84fb3e619305760131318c4a2dbaede84133abbe4d21fd08f9ab8a4fb4d91300"
+ }
+ ]
+ },
+ "engine": {
+ "comServer": "TBD.Document",
+ "executable": {
+ "path": "C:\\Program Files\\Environmental Design Solutions Ltd\\Tas\\TBD.exe",
+ "sha256": "sha256:aa658fcdeec8f5e3acbd64523376b5a7362c499beecc280715edca64851e49ae",
+ "fileVersion": "2, 0, 0, 1",
+ "productVersion": "9.5.7.0"
+ },
+ "version": "9.5.7.0"
+ },
+ "weather": {
+ "source": "Explicit",
+ "name": "HUN_CEN_Budapest.Met.Center, 128400, SRC-TMYx",
+ "contentSha256": "sha256:357bc8af0a1eb05480eba6489b3dbc37236624f198b1f4108fff3067efc9f175",
+ "contentHashOf": "the SAM WeatherData JSON given to TAS, canonical JSON v1 (SAM_Validation)",
+ "file": {
+ "path": "C:\\TasOut\\s01b\\inputs\\Budapest.epw",
+ "sha256": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3"
+ }
+ },
+ "designDays": {
+ "source": "DerivedFromWeather",
+ "heating": [
+ "Budapest.Met.Center ANN HTG 100% CONDS DB"
+ ],
+ "cooling": [
+ "Budapest.Met.Center ANN CLG 0% CONDS DB=\u003EGRad"
+ ],
+ "usedForSizing": true
+ },
+ "settings": {
+ "inputs": {
+ "simulate": true,
+ "fullYearSimulation": true,
+ "fullYearSimulationFrom": 1,
+ "fullYearSimulationTo": 365,
+ "sizing": true,
+ "unmetHours": true,
+ "useWidths": false,
+ "solarCalculationMethod": "TAS",
+ "directT3D": true,
+ "updateConstructionLayersByPanelType": false,
+ "createTM59": false,
+ "createSAP": false,
+ "createTPD": false,
+ "createPartL": false,
+ "printRoomDataSheets": false,
+ "zoneCategory": "(none)"
+ },
+ "workflow": {
+ "_type": "SAM.Analytical.Tas.WorkflowSettings,SAM.Analytical.Tas",
+ "Path_TBD": "C:\\TasOut\\s03\\tas-explicit\\box.tbd",
+ "T3DRoute": "Direct",
+ "WeatherData": {
+ "name": "HUN_CEN_Budapest.Met.Center, 128400, SRC-TMYx",
+ "contentSha256": "sha256:357bc8af0a1eb05480eba6489b3dbc37236624f198b1f4108fff3067efc9f175"
+ },
+ "DesignDays_Heating": [
+ {
+ "_type": "SAM.Analytical.DesignDay,SAM.Analytical",
+ "Data": [
+ {
+ "Name": "DryBulbTemperature",
+ "Values": [
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9,
+ -10.9
+ ]
+ },
+ {
+ "Name": "WetBulbTemperature",
+ "Values": [
+ -16.3,
+ -16.5,
+ -16.6,
+ -16.3,
+ -15.4,
+ -15.2,
+ -16.1,
+ -16.2,
+ -15.9,
+ -14.5,
+ -15.2,
+ -13.5,
+ -14.4,
+ -14.5,
+ -14.5,
+ -15,
+ -15.8,
+ -15.1,
+ -14.1,
+ -13.8,
+ -13.8,
+ -13,
+ -12.8,
+ -14.6
+ ]
+ },
+ {
+ "Name": "RelativeHumidity",
+ "Values": [
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65,
+ 65
+ ]
+ },
+ {
+ "Name": "AtmosphericPressure",
+ "Values": [
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310,
+ 100310
+ ]
+ },
+ {
+ "Name": "GlobalSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "DiffuseSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "WindDirection",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "WindSpeed",
+ "Values": [
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4,
+ 2.4
+ ]
+ },
+ {
+ "Name": "CloudCover",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "Name": "Budapest.Met.Center ANN HTG 100% CONDS DB",
+ "Description": "Heating Design Day automated :) \n *calculated from weather data as day with min temp - single value and its wind speed all remaining values are zero",
+ "Year": 2018,
+ "Month": 2,
+ "Day": 8
+ }
+ ],
+ "DesignDays_Cooling": [
+ {
+ "_type": "SAM.Analytical.DesignDay,SAM.Analytical",
+ "Data": [
+ {
+ "Name": "DryBulbTemperature",
+ "Values": [
+ 21.1,
+ 21.7,
+ 22.4,
+ 22.6,
+ 23.1,
+ 23.8,
+ 22.1,
+ 23.9,
+ 25.6,
+ 27.2,
+ 28.9,
+ 30.6,
+ 32.2,
+ 33.9,
+ 33.2,
+ 32.6,
+ 32,
+ 31.3,
+ 24.4,
+ 23.3,
+ 22.8,
+ 22.9,
+ 23,
+ 23.1
+ ]
+ },
+ {
+ "Name": "WetBulbTemperature",
+ "Values": [
+ 19.1,
+ 19.6,
+ 20,
+ 20.7,
+ 20.4,
+ 20.1,
+ 21.9,
+ 19.4,
+ 18.9,
+ 18.3,
+ 17.8,
+ 17.2,
+ 16.6,
+ 16.1,
+ 15.7,
+ 15.1,
+ 14.6,
+ 14,
+ 24.2,
+ 23.2,
+ 21.7,
+ 20.7,
+ 19.5,
+ 18.7
+ ]
+ },
+ {
+ "Name": "RelativeHumidity",
+ "Values": [
+ 89,
+ 88,
+ 86,
+ 89,
+ 85,
+ 80,
+ 100,
+ 76,
+ 66,
+ 58,
+ 51,
+ 45,
+ 39,
+ 35,
+ 35,
+ 35,
+ 35,
+ 35,
+ 99,
+ 99,
+ 93,
+ 87,
+ 80,
+ 76
+ ]
+ },
+ {
+ "Name": "AtmosphericPressure",
+ "Values": [
+ 99788,
+ 99758,
+ 99718,
+ 99733,
+ 99677,
+ 99631,
+ 99825,
+ 99544,
+ 99514,
+ 99484,
+ 99455,
+ 99425,
+ 99395,
+ 99365,
+ 99333,
+ 99300,
+ 99267,
+ 99235,
+ 99709,
+ 99788,
+ 99829,
+ 99830,
+ 99830,
+ 99831
+ ]
+ },
+ {
+ "Name": "GlobalSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 22,
+ 101,
+ 141,
+ 251,
+ 523,
+ 739,
+ 851,
+ 903,
+ 896,
+ 835,
+ 680,
+ 501,
+ 357,
+ 208,
+ 71,
+ 4,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "DiffuseSolarRadiation",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 22,
+ 83,
+ 123,
+ 200,
+ 259,
+ 205,
+ 154,
+ 177,
+ 211,
+ 193,
+ 230,
+ 243,
+ 179,
+ 120,
+ 49,
+ 4,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "Name": "WindDirection",
+ "Values": [
+ 140,
+ 138,
+ 159,
+ 198,
+ 177,
+ 195,
+ 168,
+ 84,
+ 263,
+ 204,
+ 23,
+ 77,
+ 315,
+ 164,
+ 277,
+ 95,
+ 27,
+ 292,
+ 97,
+ 160,
+ 79,
+ 147,
+ 74,
+ 40
+ ]
+ },
+ {
+ "Name": "WindSpeed",
+ "Values": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.8,
+ 0.5,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0.8,
+ 1.5,
+ 2.3,
+ 3.1,
+ 3.8,
+ 4.6,
+ 4.6,
+ 4.6,
+ 4.6,
+ 4.6
+ ]
+ },
+ {
+ "Name": "CloudCover",
+ "Values": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ }
+ ],
+ "Name": "Budapest.Met.Center ANN CLG 0% CONDS DB=\u003EGRad",
+ "Description": "Cooling Design Day automated :) \n *calculated from weather data as day with max temp and then radation replaced from max global radation day and cloud cover removed",
+ "Year": 2018,
+ "Month": 7,
+ "Day": 1
+ }
+ ],
+ "SurfaceOutputSpecs": [
+ {
+ "_type": "SAM.Core.Tas.SurfaceOutputSpec,SAM.Core.Tas",
+ "Name": "Tas.Simulate",
+ "Guid": "daa4cdb1-2aeb-454f-86fb-c0653d9c8334",
+ "ApertureData": true,
+ "Condensation": false,
+ "Convection": false,
+ "SolarGain": true,
+ "Conduction": true,
+ "LongWave": false,
+ "Temperature": true
+ }
+ ],
+ "UnmetHours": true,
+ "Simulate": true,
+ "Sizing": true,
+ "UpdateZones": true,
+ "UseWidths": false,
+ "AddIZAMs": true,
+ "RemoveIZAMs": false,
+ "RemoveMechanicalVentilationGains": false,
+ "SimulateFrom": 1,
+ "SimulateTo": 365,
+ "RemoveExistingTBD": false,
+ "UpdateWindowPositionType": false
+ }
+ },
+ "unknown": {
+ "application.sourceRevision": "this build\u0027s InformationalVersion \u00271.0.0\u0027 has no source revision after \u0027\u002B\u0027 (a local developer build); the component SHA-256 values identify the binaries",
+ "application.components[SAM.Analytical.Tas].informationalVersion": "the assembly carries no InformationalVersion"
+ }
+ }
+}
\ No newline at end of file
diff --git a/documentation/evidence/s03-20261019/tools/S03EvidenceHarness.cs.txt b/documentation/evidence/s03-20261019/tools/S03EvidenceHarness.cs.txt
new file mode 100644
index 00000000..3ded6c40
--- /dev/null
+++ b/documentation/evidence/s03-20261019/tools/S03EvidenceHarness.cs.txt
@@ -0,0 +1,135 @@
+// Temporary, NOT committed: produces S03 representative manifests from real engines on a licensed machine.
+#nullable enable
+
+using SAM.Core;
+using SAM.Weather;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SAM.Analytical.UI.WPF.Tests
+{
+ [Collection(WpfCollection.Name)]
+ public class S03EvidenceHarness
+ {
+ private const string Inputs = @"C:\TasOut\s01b\inputs";
+ private const string Root = @"C:\TasOut\s03";
+ private readonly ITestOutputHelper output;
+
+ public S03EvidenceHarness(ITestOutputHelper output)
+ {
+ this.output = output;
+ }
+
+ private static bool Enabled => Environment.GetEnvironmentVariable("S03_EVIDENCE") == "1";
+
+ [WpfFact]
+ public void E1_RealTas_ExplicitEpw()
+ {
+ if (!Enabled) return;
+ RunTas("tas-explicit", explicitEpw: true);
+ }
+
+ [WpfFact]
+ public void E2_RealTas_EmbeddedWeather()
+ {
+ if (!Enabled) return;
+ RunTas("tas-embedded", explicitEpw: false);
+ }
+
+ private void RunTas(string name, bool explicitEpw)
+ {
+ string directory = Path.Combine(Root, name);
+ Directory.CreateDirectory(directory);
+ string modelPath = Path.Combine(directory, "box-aperture.json");
+ File.Copy(Path.Combine(Inputs, "box-aperture.json"), modelPath, true);
+
+ UIAnalyticalModel uIAnalyticalModel = new UIAnalyticalModel(modelPath);
+ Assert.True(uIAnalyticalModel.Open());
+
+ string epw = Path.Combine(Inputs, "Budapest.epw");
+ WeatherData? weatherData;
+ if (explicitEpw)
+ {
+ Assert.True(UI.Query.TryGetWeatherData(epw, out weatherData));
+ }
+ else
+ {
+ Assert.True(uIAnalyticalModel.JSAMObject.TryGetValue(Analytical.AnalyticalModelParameter.WeatherData, out weatherData));
+ weatherData = new WeatherData(weatherData);
+ }
+
+ PartOSimulationOutcome outcome = Modify.Simulate(uIAnalyticalModel, "box", directory, weatherData!, explicitEpw ? epw : null, null);
+ output.WriteLine("Ran={0} Cancelled={1} Failure={2} Refusal={3}", outcome.Ran, outcome.Cancelled, outcome.Failure, outcome.Refusal);
+ output.WriteLine(File.ReadAllText(Path.Combine(directory, "box.tas-run.json")));
+ }
+
+ [WpfFact]
+ public void E3_RealBoth_WithoutSamCommit()
+ {
+ if (!Enabled) return;
+ RunBoth("both-no-samcommit", null);
+ }
+
+ [WpfFact]
+ public void E4_RealBoth_WithManualSamCommit()
+ {
+ if (!Enabled) return;
+ RunBoth("both-manual-samcommit", "62b5415697d1a5af143c6b581b7d42fbd6551ca8");
+ }
+
+ private void RunBoth(string name, string? samCommit)
+ {
+ string directory = Path.Combine(Root, name);
+ Directory.CreateDirectory(directory);
+ AnalyticalModel analyticalModel = Core.Convert.ToSAM(Path.Combine(Inputs, "box-aperture.json")).First();
+
+ string? samCommit_Before = Environment.GetEnvironmentVariable("SAM_COMMIT");
+ string? path_Before = Environment.GetEnvironmentVariable("PATH");
+ Environment.SetEnvironmentVariable("SAM_COMMIT", samCommit);
+ Environment.SetEnvironmentVariable("PATH", @"C:\Users\michal\Documents\GitHub\SAM-BIM\.icon-migration\runtime\OpenStudio-3.10.0+86d7e215a1-Windows\bin;" + path_Before);
+ try
+ {
+ EngineComparisonRun run = analyticalModel.RunEngineComparison(directory, Path.Combine(Inputs, "Budapest.epw"),
+ @"C:\Users\michal\Documents\GitHub\SAM-BIM\SAM_Tas\build\benchmark-tas.exe",
+ @"C:\Users\michal\Documents\GitHub\SAM-BIM\SAM_OpenStudio\benchmark\SAM.Analytical.OpenStudio.Benchmark\bin\x64\Release\net8.0\benchmark-openstudio.exe",
+ Path.Combine(Inputs, "box-aperture.json"));
+ output.WriteLine("Status={0} Manifest={1}", run.Status, run.ManifestPath);
+ output.WriteLine(File.ReadAllText(run.ManifestPath!));
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("SAM_COMMIT", samCommit_Before);
+ Environment.SetEnvironmentVariable("PATH", path_Before);
+ }
+ }
+ }
+}
+
+namespace SAM.Analytical.UI.WPF.Tests
+{
+ public class S03EvidenceHarnessOpenStudio
+ {
+ private readonly Xunit.Abstractions.ITestOutputHelper output;
+ public S03EvidenceHarnessOpenStudio(Xunit.Abstractions.ITestOutputHelper output) { this.output = output; }
+
+ [Xunit.Fact]
+ public void E5_RealOpenStudio_ExplicitEpw() { if (System.Environment.GetEnvironmentVariable("S03_EVIDENCE") == "1") Run("openstudio-explicit", true); }
+
+ [Xunit.Fact]
+ public void E6_RealOpenStudio_Embedded() { if (System.Environment.GetEnvironmentVariable("S03_EVIDENCE") == "1") Run("openstudio-embedded", false); }
+
+ private void Run(string name, bool explicitEpw)
+ {
+ string directory = System.IO.Path.Combine(@"C:\TasOut\s03", name);
+ System.IO.Directory.CreateDirectory(directory);
+ string modelPath = @"C:\TasOut\s01b\inputs\box-aperture.json";
+ AnalyticalModel analyticalModel = Core.Convert.ToSAM(modelPath)[0];
+ OpenStudioSimulationRun run = analyticalModel.RunOpenStudioSimulation(directory, explicitEpw ? @"C:\TasOut\s01b\inputs\Budapest.epw" : null, modelPath);
+ output.WriteLine("Status={0} Manifest={1}", run.Status, run.ManifestPath);
+ }
+ }
+}