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); + } + } +}