diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs new file mode 100644 index 00000000..cd174ed0 --- /dev/null +++ b/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +#nullable enable + +using SAM.Analytical.Benchmark; +using SAM.Core; +using SAM.Geometry.UI; +using SAM.Weather; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading; +using Xunit; + +namespace SAM.Analytical.UI.WPF.Tests +{ + /// + /// S04 (finding F7 of SAM_UI#239): the frozen simulation snapshot - the file both Run Simulation > Both producers hash and + /// OpenStudio Simulation converts - leaves out the one UI-only parameter SAM_UI keeps on a model, "UI Geometry Settings" (its + /// views, regenerated with new GUIDs every time a model is opened), so the same engineering model gets the same snapshot and the + /// same canonical model hash in every session, while every engineering edit still changes it. + /// + /// The canonical hash is computed here exactly as the producers compute it (load the snapshot into an AnalyticalModel, then + /// SAM_Validation's canonical JSON v1 of its JSON); the models are saved, reopened through as the + /// window opens them, and frozen by the real . + /// + /// + [Collection(WpfCollection.Name)] + public class CanonicalSnapshotTests : IDisposable + { + private readonly string directory; + + public CanonicalSnapshotTests() + { + directory = Path.Combine(Path.GetTempPath(), "SAM-UI-S04-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) + { + } + } + + /// A. The same saved model, opened twice as the window opens it: the UI views differ, the snapshots and hashes do not. + [WpfFact] + public void SameSavedModel_OpenedTwice_GivesTheSameSnapshotAndCanonicalHash() + { + string path = Save(Model(), "model.json"); + + AnalyticalModel opened_1 = Open(path); + AnalyticalModel opened_2 = Open(path); + + //The F7 precondition: opening regenerated the UI view state, with different GUIDs each time. + Assert.NotEqual(ViewGuids(opened_1), ViewGuids(opened_2)); + + string snapshot_1 = Freeze(opened_1, "1"); + string snapshot_2 = Freeze(opened_2, "2"); + + Assert.Equal(Query.Sha256(snapshot_1), Query.Sha256(snapshot_2)); + Assert.Equal(ProducerCanonicalHash(snapshot_1), ProducerCanonicalHash(snapshot_2)); + } + + /// B. The same model with different UI-only view settings (new GUIDs, another view, a hidden object): same hash. + [WpfFact] + public void DifferentUiViewState_GivesTheSameCanonicalHash() + { + AnalyticalModel analyticalModel_1 = Model(); + AnalyticalModel analyticalModel_2 = new AnalyticalModel(analyticalModel_1); + analyticalModel_1.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + analyticalModel_2.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View", "Plan")); + + Assert.NotEqual(Core.Convert.ToString(analyticalModel_1), Core.Convert.ToString(analyticalModel_2)); + Assert.Equal(ProducerCanonicalHash(Freeze(analyticalModel_1, "1")), ProducerCanonicalHash(Freeze(analyticalModel_2, "2"))); + } + + /// C. Every kind of engineering edit still changes the snapshot and the canonical hash. + [WpfTheory] + [InlineData("construction")] + [InlineData("space")] + [InlineData("weather")] + [InlineData("modelParameter")] + [InlineData("otherUiAssemblyParameterName")] + public void AnEngineeringEdit_ChangesTheCanonicalHash(string edit) + { + AnalyticalModel analyticalModel = Model(); + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + string hash_Before = ProducerCanonicalHash(Freeze(analyticalModel, "before")); + + AnalyticalModel analyticalModel_Edited = Edit(analyticalModel, edit); + analyticalModel_Edited.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + + Assert.NotEqual(hash_Before, ProducerCanonicalHash(Freeze(analyticalModel_Edited, "after"))); + } + + /// + /// The snapshot is the source model less exactly one parameter: nothing else is dropped, reordered or regenerated (the omission + /// direction of the mutation check). + /// + [WpfFact] + public void Snapshot_IsTheModelLessOnlyTheUiViewState() + { + AnalyticalModel analyticalModel = Model(); + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + + //Expected: the model's JSON less the one parameter, and less its set because that leaves it empty. + JsonObject expected = analyticalModel.ToJsonObject(); + JsonArray parameterSets = expected["ParameterSets"]!.AsArray(); + JsonObject uiParameterSet = parameterSets.Select(x => x!.AsObject()).Single(x => (string)x["Name"]! == "SAM.Analytical.UI"); + JsonArray parameters = uiParameterSet["Parameters"]!.AsArray(); + parameters.Remove(parameters.Single(x => (string)x!["Name"]! == "UI Geometry Settings")); + Assert.Empty(parameters); + parameterSets.Remove(uiParameterSet); + int count_ParameterSets = parameterSets.Count; + + AnalyticalModel snapshot = Core.Convert.ToSAM(Freeze(analyticalModel, "snapshot")).Single(); + + Assert.Equal(new AnalyticalModel(expected).ToJsonObject().ToJsonString(), snapshot.ToJsonObject().ToJsonString()); + Assert.False(snapshot.HasValue(AnalyticalModelParameter.UIGeometrySettings)); + Assert.True(snapshot.HasValue(Analytical.AnalyticalModelParameter.WeatherData)); + Assert.Equal(count_ParameterSets, snapshot.GetParameterSets().Count); + } + + /// + /// A model whose view state was removed keeps an empty SAM.Analytical.UI set - written with no "Parameters" at all, and still carrying + /// the per-build GUID. It is left out like an emptied one, so it freezes exactly as a model that never had the set. + /// + [WpfFact] + public void AnAlreadyEmptyUiParameterSet_IsLeftOutToo() + { + AnalyticalModel analyticalModel_Never = Model(); + AnalyticalModel analyticalModel = new AnalyticalModel(analyticalModel_Never); + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + Assert.True(analyticalModel.RemoveValue(AnalyticalModelParameter.UIGeometrySettings)); + Assert.Contains(analyticalModel.ToJsonObject()["ParameterSets"]!.AsArray(), x => (string)x!["Name"]! == "SAM.Analytical.UI"); + + Assert.Equal(File.ReadAllBytes(Freeze(analyticalModel_Never, "never")), File.ReadAllBytes(Freeze(analyticalModel, "emptied"))); + } + + /// A model with no view state is frozen byte for byte as before S04 (Core.Convert.ToFile's JSON). + [WpfFact] + public void Snapshot_OfAModelWithNoViewState_IsTheSameBytesAsBefore() + { + AnalyticalModel analyticalModel = Model(); + Assert.False(analyticalModel.HasValue(AnalyticalModelParameter.UIGeometrySettings)); + + string path_Before = Path.Combine(directory, "pre-s04.json"); + Assert.True(Core.Convert.ToFile(analyticalModel, path_Before)); + + Assert.Equal(File.ReadAllBytes(path_Before), File.ReadAllBytes(Freeze(analyticalModel, "s04"))); + } + + /// + /// The view state's set carries the SAM.Analytical.UI assembly's GUID - its module version id, new with every build of SAM_UI. Two + /// builds' copies of one model (simulated by giving that set another GUID) still give the same snapshot and canonical hash. + /// + [WpfFact] + public void ViewStateFromAnotherBuildOfSamUi_GivesTheSameCanonicalHash() + { + AnalyticalModel analyticalModel = Model(); + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + + JsonObject jsonObject = analyticalModel.ToJsonObject(); + JsonObject uiParameterSet = jsonObject["ParameterSets"]!.AsArray().Select(x => x!.AsObject()).Single(x => (string)x["Name"]! == "SAM.Analytical.UI"); + Assert.NotEqual(Guid.Empty.ToString(), (string)uiParameterSet["Guid"]!); + uiParameterSet["Guid"] = Guid.NewGuid().ToString(); + AnalyticalModel analyticalModel_OtherBuild = new AnalyticalModel(jsonObject); + + string snapshot_1 = Freeze(analyticalModel, "1"); + string snapshot_2 = Freeze(analyticalModel_OtherBuild, "2"); + + Assert.Equal(Query.Sha256(snapshot_1), Query.Sha256(snapshot_2)); + Assert.Equal(ProducerCanonicalHash(snapshot_1), ProducerCanonicalHash(snapshot_2)); + } + + /// G. Freezing never changes the model given - not its view state, not anything else. + [WpfFact] + public void Freezing_DoesNotChangeTheSourceModel() + { + AnalyticalModel analyticalModel = Model(); + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + string json_Before = Core.Convert.ToString(analyticalModel); + + Freeze(analyticalModel, "1"); + + Assert.Equal(json_Before, Core.Convert.ToString(analyticalModel)); + Assert.True(analyticalModel.HasValue(AnalyticalModelParameter.UIGeometrySettings)); + } + + /// + /// D, E. Two Both runs of the same model opened in two sessions feed their producers byte-identical snapshots: the snapshot hash + /// in each manifest is its file's, and both producers of one run read that one file. + /// + [WpfFact] + public void TwoBothRuns_OfTheSameModelOpenedTwice_FeedIdenticalSnapshots() + { + string path = Save(Model(), "model.json"); + string epw = Path.Combine(directory, "test.epw"); + File.WriteAllText(epw, "LOCATION,Test,,,TMY,000000,0,0,0,0\n"); + + List runs = new List(); + foreach (string session in new[] { "session-1", "session-2" }) + { + List models = new List(); + EngineComparisonRun run = Modify.RunEngineComparison(Open(path), Path.Combine(directory, session), epw, Producer("benchmark-tas.exe"), Producer("benchmark-openstudio.exe"), path, null, CancellationToken.None, (producerRun, workDirectory, cancellationToken) => + { + models.Add(producerRun.Arguments[producerRun.Arguments.IndexOf("--model") + 1]); + return 5; + }); + + Assert.Equal(new[] { run.SnapshotPath, run.SnapshotPath }, models); + Assert.Equal(Query.Sha256(run.SnapshotPath), run.SnapshotSha256); + Assert.Equal(run.SnapshotSha256, (string)JsonNode.Parse(File.ReadAllText(run.ManifestPath!))!["snapshot"]!["sha256"]!); + runs.Add(run); + } + + Assert.NotEqual(runs[0].SnapshotPath, runs[1].SnapshotPath); + Assert.Equal(runs[0].SnapshotSha256, runs[1].SnapshotSha256); + Assert.Equal(ProducerCanonicalHash(runs[0].SnapshotPath!), ProducerCanonicalHash(runs[1].SnapshotPath!)); + } + + /// F. The OpenStudio result model, built from the frozen copy, still carries the open model's view state, as before S04. + [WpfFact] + public void OpenStudioResultModel_KeepsTheSourceViewState() + { + AnalyticalModel analyticalModel = Model(); + UIGeometrySettings uIGeometrySettings = ViewState("3D View", "Plan"); + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, uIGeometrySettings); + + AnalyticalModel analyticalModel_Frozen = Core.Convert.ToSAM(Freeze(analyticalModel, "frozen")).Single(); + Assert.False(analyticalModel_Frozen.HasValue(AnalyticalModelParameter.UIGeometrySettings)); + + //Copied at freeze time: a view edited afterwards on the open model does not reach the result model. + UIGeometrySettings? viewState = Query.ViewState(analyticalModel); + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("Edited after the freeze")); + + AnalyticalModel analyticalModel_Result = Modify.WithViewState(analyticalModel_Frozen, viewState); + + Assert.True(analyticalModel_Result.TryGetValue(AnalyticalModelParameter.UIGeometrySettings, out UIGeometrySettings uIGeometrySettings_Result)); + Assert.Equal(uIGeometrySettings.ToJsonObject().ToJsonString(), uIGeometrySettings_Result.ToJsonObject().ToJsonString()); + Assert.NotSame(uIGeometrySettings, uIGeometrySettings_Result); + } + + // ---------------------------------------------------------------- helpers + + /// As benchmark-tas and benchmark-openstudio compute canonicalModelHash: the snapshot loaded, then canonical JSON v1 of its JSON. + private static string ProducerCanonicalHash(string path) + { + AnalyticalModel analyticalModel = Core.Convert.ToSAM(path).First(); + return BenchmarkCanonicalJson.ComputeSha256(analyticalModel.ToJsonObject().ToJsonString()); + } + + private string Freeze(AnalyticalModel analyticalModel, string name) + { + string path = Path.Combine(directory, "snapshot-" + name + ".json"); + Assert.NotNull(analyticalModel.FreezeAnalyticalModel(path)); + return path; + } + + private string Save(AnalyticalModel analyticalModel, string name) + { + string path = Path.Combine(directory, name); + Assert.True(Core.Convert.ToFile(new IJSAMObject[] { analyticalModel }, path)); + return path; + } + + /// + /// Opens a saved model as the window does: , then what AnalyticalWindow.SetDefaultViewSettings + /// does on Opened - a model with no views gets UI.Query.DefaultViewSettings, whose GUID is new every time (F7). + /// + private static AnalyticalModel Open(string path) + { + UIAnalyticalModel uIAnalyticalModel = new UIAnalyticalModel(path); + Assert.True(uIAnalyticalModel.Open()); + + AnalyticalModel analyticalModel = uIAnalyticalModel.JSAMObject; + if (!analyticalModel.TryGetValue(AnalyticalModelParameter.UIGeometrySettings, out UIGeometrySettings uIGeometrySettings) || uIGeometrySettings == null) + { + uIGeometrySettings = new UIGeometrySettings(); + } + + if (uIGeometrySettings.GetViewSettings() is not { Count: > 0 }) + { + ViewSettings viewSettings = UI.Query.DefaultViewSettings(analyticalModel); + uIGeometrySettings.AddViewSettings(viewSettings); + uIGeometrySettings.ActiveGuid = viewSettings.Guid; + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, uIGeometrySettings); + } + + return analyticalModel; + } + + private string Producer(string fileName) + { + string path = Path.Combine(directory, "producers", fileName); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, string.Empty); + return path; + } + + private static string ViewGuids(AnalyticalModel analyticalModel) + { + Assert.True(analyticalModel.TryGetValue(AnalyticalModelParameter.UIGeometrySettings, out UIGeometrySettings uIGeometrySettings)); + return uIGeometrySettings.ToJsonObject().ToJsonString(); + } + + /// A realistic multizone model: the Part O fixture's flats (spaces, panels, apertures, constructions, internal conditions), with weather. + private static AnalyticalModel Model() + { + AnalyticalModel analyticalModel = PartOMixedDesignFixture.Baseline(2); + analyticalModel.SetValue(Analytical.AnalyticalModelParameter.WeatherData, new WeatherData("Fixture", "Fixture", 51.5, -0.1, 25)); + return analyticalModel; + } + + private static AnalyticalModel Edit(AnalyticalModel analyticalModel, string edit) + { + AnalyticalModel result = new AnalyticalModel(analyticalModel, true); + switch (edit) + { + case "construction": + AdjacencyCluster adjacencyCluster = result.AdjacencyCluster; + Construction construction = new Construction(Guid.NewGuid(), "Thicker", new List { new ConstructionLayer("Plasterboard", 0.2) }); + Panel panel = adjacencyCluster.GetPanels().First(); + adjacencyCluster.AddObject(Analytical.Create.Panel(panel, construction)); + return new AnalyticalModel(result, adjacencyCluster); + case "space": + AdjacencyCluster adjacencyCluster_Space = result.AdjacencyCluster; + Space space = adjacencyCluster_Space.GetSpaces().First(); + Space space_Edited = new Space(space); + space_Edited.SetValue(SpaceParameter.Volume, 123.0); + adjacencyCluster_Space.AddObject(space_Edited); + return new AnalyticalModel(result, adjacencyCluster_Space); + case "weather": + result.SetValue(Analytical.AnalyticalModelParameter.WeatherData, new WeatherData("Fixture", "Fixture", 52.5, -0.1, 25)); + return result; + case "modelParameter": + result.SetValue(Analytical.AnalyticalModelParameter.NorthAngle, 0.5); + return result; + case "otherUiAssemblyParameterName": + //Anything else stored in the SAM.Analytical.UI set is kept: only the named view state is UI-only. + result.SetValue(AnalyticalModelParameter.UIGeometrySettings, ViewState("3D View")); + Assert.True(result.GetParameterSet(typeof(AnalyticalModelParameter).Assembly).Add("Another UI parameter", "kept")); + return result; + } + + throw new ArgumentOutOfRangeException(nameof(edit)); + } + + private static UIGeometrySettings ViewState(params string[] names) + { + UIGeometrySettings result = new UIGeometrySettings(); + foreach (string name in names) + { + result.AddViewSettings(new ThreeDimensionalViewSettings(Guid.NewGuid(), name, null, null, null)); + } + + return result; + } + } +} diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs index e92ef2f1..48ac00a1 100644 --- a/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs +++ b/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs @@ -239,6 +239,12 @@ public void RealRun_SimulatesAFrozenCopy_AndSavesAResultModelWithOnlyOpenStudioR } AnalyticalModel analyticalModel = Core.Convert.ToSAM(modelPath).First(); + + //The view state SAM_UI keeps on an open model (S04: left out of the snapshot, given back to the result model). + SAM.Geometry.UI.UIGeometrySettings uIGeometrySettings = new SAM.Geometry.UI.UIGeometrySettings(); + uIGeometrySettings.AddViewSettings(SAM.Analytical.UI.Query.DefaultViewSettings(analyticalModel)); + analyticalModel.SetValue(SAM.Analytical.UI.AnalyticalModelParameter.UIGeometrySettings, uIGeometrySettings); + string json_Before = Core.Convert.ToString(analyticalModel); OpenStudioSimulationRun run = analyticalModel.RunOpenStudioSimulation(directory, epwPath, modelPath); @@ -258,6 +264,13 @@ public void RealRun_SimulatesAFrozenCopy_AndSavesAResultModelWithOnlyOpenStudioR Assert.All(results, x => Assert.Equal(AnalyticalModelSimulationResult.OpenStudioSource, x.Source)); Assert.Single(result.GetAnalyticalModelSimulationResults()); + //S04: the snapshot carries no view state; the result model carries the open model's, as before. + Assert.False(Core.Convert.ToSAM(run.SnapshotPath).First().HasValue(SAM.Analytical.UI.AnalyticalModelParameter.UIGeometrySettings)); + Assert.True(result.TryGetValue(SAM.Analytical.UI.AnalyticalModelParameter.UIGeometrySettings, out SAM.Geometry.UI.UIGeometrySettings uIGeometrySettings_Result)); + //Compared after the same JSON round trip the saved file makes: UIGeometrySettings itself does not round-trip its default + //view's appearance dictionary, before S04 as after. + Assert.Equal(new SAM.Geometry.UI.UIGeometrySettings(JsonNode.Parse(uIGeometrySettings.ToJsonObject().ToJsonString()).AsObject()).ToJsonObject().ToJsonString(), uIGeometrySettings_Result.ToJsonObject().ToJsonString()); + //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(); diff --git a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs index 0de519df..f0e06eea 100644 --- a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs +++ b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs @@ -76,9 +76,20 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod result.SnapshotSha256 = Query.Sha256(result.SnapshotPath); + //S04: the open model's view state, copied at the moment of the snapshot, for the result model; never a reason to fail. + Geometry.UI.UIGeometrySettings? viewState = null; + try + { + viewState = Query.ViewState(analyticalModel); + } + catch (Exception exception) + { + result.Diagnostics.Add("Warning: the view state could not be copied for the result model: " + exception.GetType().Name + ": " + exception.Message); + } + cancellationToken.ThrowIfCancellationRequested(); - RunOpenStudioSimulation_Run(result, analyticalModel_Frozen, epwPath, progress, cancellationToken); + RunOpenStudioSimulation_Run(result, analyticalModel_Frozen, viewState, epwPath, progress, cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -102,15 +113,31 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod /// /// Writes to and returns the model read back from that file: an /// independent copy whose content is exactly the file's. Null when either step fails. + /// + /// The file is of the model (S04): everything except SAM_UI's view state, which is + /// regenerated on every open and would otherwise give the same model a different snapshot and canonical hash in each session. + /// A JSON snapshot is written exactly as Core.Convert.ToFile writes a JSON file, so a model with no view state gives the + /// same bytes as before S04. + /// /// public static AnalyticalModel? FreezeAnalyticalModel(this AnalyticalModel analyticalModel, string path) { - if (analyticalModel == null || string.IsNullOrWhiteSpace(path)) + if (analyticalModel == null || string.IsNullOrWhiteSpace(path) || !System.IO.Directory.Exists(Path.GetDirectoryName(path))) + { + return null; + } + + JsonObject? jsonObject = Query.SimulationSnapshotJsonObject(analyticalModel); + if (jsonObject == null) { return null; } - if (!Core.Convert.ToFile(analyticalModel, path)) + if (Core.Query.SAMFileType(path) == Core.SAMFileType.Json) + { + File.WriteAllText(path, new JsonArray(jsonObject).ToJsonString(Core.Convert.SerializerOptions(Core.Formatting.Indented))); + } + else if (!Core.Convert.ToFile(new AnalyticalModel(jsonObject), path)) { return null; } @@ -154,7 +181,7 @@ public static AnalyticalModel AddOpenStudioResults(this AnalyticalModel analytic // Its own method, never inlined: every OpenStudio type is reached here, so a missing SAM_OpenStudio or OpenStudio assembly fails // this method's compilation inside the caller's try, and is recorded on the run like any other failure. [MethodImpl(MethodImplOptions.NoInlining)] - private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStudioSimulationRun, AnalyticalModel analyticalModel_Frozen, string? epwPath, IProgress? progress, CancellationToken cancellationToken) + private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStudioSimulationRun, AnalyticalModel analyticalModel_Frozen, Geometry.UI.UIGeometrySettings? viewState, string? epwPath, IProgress? progress, CancellationToken cancellationToken) { openStudioSimulationRun.SamOpenStudioVersion = typeof(OpenStudioConversionResult).Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(OpenStudioConversionResult).Assembly.GetName().Version?.ToString(); @@ -265,6 +292,17 @@ private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStud openStudioSimulationRun.ResultDiagnostics.AddRange(diagnostics); } + //S04: the snapshot left the open model's view state out; the result model, which a person opens, gets back the copy taken + //with the snapshot. A failure here is a warning, never a failed run. + try + { + WithViewState(analyticalModel_Result, viewState); + } + catch (Exception exception) + { + openStudioSimulationRun.Diagnostics.Add("Warning: the result model was saved without the view state: " + exception.GetType().Name + ": " + exception.Message); + } + string resultModelPath = Path.Combine(openStudioSimulationRun.Directory!, Query.OpenStudioResultModelFileName(analyticalModel_Frozen.Name)); if (!Core.Convert.ToFile(analyticalModel_Result, resultModelPath)) { diff --git a/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs b/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs new file mode 100644 index 00000000..e06a9810 --- /dev/null +++ b/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using System.Linq; +using System.Text.Json.Nodes; + +namespace SAM.Analytical.UI.WPF +{ + public static partial class Query + { + /// + /// The JSON of as a frozen simulation snapshot holds it (roadmap S04, finding F7 of SAM_UI#239): + /// the model's own JSON without UI Geometry Settings, the view state SAM_UI keeps on a model (its views, their visibility, + /// cameras and legends). + /// + /// That parameter never reaches a simulation, and SAM_UI regenerates it - with new GUIDs - every time a model is opened, so a + /// snapshot that carried it gave the same engineering model a different snapshot hash and canonical model hash in every session. + /// It is the only parameter left out; the same parameter is already left out of SimulationResultProvenance.Fingerprint in + /// SAM. Its parameter set (SAM.Analytical.UI) goes too, but only when that leaves it empty: an empty set carries nothing but its + /// GUID, which is the assembly's module version id and so changes with every build of SAM_UI. Anything else stored in that set + /// stays, as does every other parameter set. + /// + /// + /// The model given is never changed: this edits the freshly built JSON tree, not the model. Null for no model. + /// + /// + public static JsonObject? SimulationSnapshotJsonObject(AnalyticalModel analyticalModel) + { + JsonObject? result = analyticalModel?.ToJsonObject(); + if (result?["ParameterSets"] is not JsonArray parameterSets) + { + return result; + } + + string name_ParameterSet = Core.Query.Name(typeof(UI.AnalyticalModelParameter).Assembly); + string? name_Parameter = Core.Attributes.ParameterProperties.Get(UI.AnalyticalModelParameter.UIGeometrySettings)?.Name; + + foreach (JsonObject parameterSet in parameterSets.OfType().Where(x => Text(x["Name"]) == name_ParameterSet).ToList()) + { + //An empty set is written without "Parameters" at all: it is dropped like an emptied one. + if (parameterSet["Parameters"] is not JsonArray parameters) + { + if (parameterSet["Parameters"] == null) + { + parameterSets.Remove(parameterSet); + } + + continue; + } + + foreach (JsonNode? parameter in parameters.Where(x => x is JsonObject jsonObject && Text(jsonObject["Name"]) == name_Parameter).ToList()) + { + parameters.Remove(parameter); + } + + if (parameters.Count == 0) + { + parameterSets.Remove(parameterSet); + } + } + + return result; + } + + private static string? Text(JsonNode? jsonNode) + { + return jsonNode is JsonValue jsonValue && jsonValue.TryGetValue(out string? value) ? value : null; + } + } + + public static partial class Query + { + /// + /// A copy of 's view state (UI Geometry Settings), taken now - at freeze time, so that later + /// edits to the open model cannot reach it - for ; null when it has none. + /// + public static Geometry.UI.UIGeometrySettings? ViewState(AnalyticalModel analyticalModel) + { + return analyticalModel != null + && analyticalModel.TryGetValue(UI.AnalyticalModelParameter.UIGeometrySettings, out Geometry.UI.UIGeometrySettings uIGeometrySettings) + && uIGeometrySettings != null + ? new Geometry.UI.UIGeometrySettings(uIGeometrySettings) + : null; + } + } + + public static partial class Modify + { + /// + /// Gives - a model derived from a snapshot, such as the OpenStudio result model - a copy of + /// ( of the open model when it was frozen), which + /// left out of the snapshot. So a result model opens with the views it opened with + /// before S04. Returns . + /// + public static AnalyticalModel WithViewState(AnalyticalModel analyticalModel, Geometry.UI.UIGeometrySettings? viewState) + { + if (analyticalModel != null && viewState != null) + { + analyticalModel.SetValue(UI.AnalyticalModelParameter.UIGeometrySettings, new Geometry.UI.UIGeometrySettings(viewState)); + } + + return analyticalModel!; + } + } +} diff --git a/documentation/S04-StableCanonicalSnapshot-PR.md b/documentation/S04-StableCanonicalSnapshot-PR.md new file mode 100644 index 00000000..b0c018ae --- /dev/null +++ b/documentation/S04-StableCanonicalSnapshot-PR.md @@ -0,0 +1,182 @@ +# S04 - Stable canonical simulation snapshot - PR record + +Roadmap: SAM `documentation/SIMULATION_ROADMAP_Q4_2026.md`, task S04 (Phase A), finding F7 of SAM_UI#239. SAM_UI only; +SAM_Validation's canonical JSON v1 and `canonicalizationVersion` are unchanged. + +Base: `sow/2026-Q4` `4d7f5bd8`. **Independent of S03** (SAM_UI#242): neither needs the other. Both append assertions to the end of +`OpenStudioSimulationTests.RealRun_…`, so whichever merges second has a textual conflict there; keep both blocks (done in a +local integration merge that passed the full suite, see the PR description). +Branch: `feature/s04-stable-canonical-snapshot`. Draft PR, not merged. **Validated content SHA: `29ca1d1a`** (code and tests, after the review fixes; first content `0594e708`). +Record commits follow it. The final PR head and hosted checks are in the PR description. `PROJECT_PROGRESS.md` is not touched. + +## Status +Implementation and local tests complete on 2026-10-19 (overnight owner-authorised S03+S04 session). On 2026-10-20 S03 +merged first (`1e460bbe`); this branch was refreshed with `sow/2026-Q4` and re-validated (see "Integration with S03"). +**Integrated content SHA: `3bbba54b`.** Awaiting its hosted CI, then merge under the owner's 2026-10-20 authorisation. + +## Problem (baseline `4d7f5bd8`), established from real evidence +- **Two sessions, one model.** S01b runs B2 and B3 (machine-local in `C:\TasOut\s01b\runs`) each froze the same + `box-aperture.json`. The two snapshots, 3,442,974 bytes each, differ in exactly two values: the `Guid` and `ActiveGuid` of + the "3D View" inside `ParameterSets[Name="SAM.Analytical.UI"].Parameters[Name="UI Geometry Settings"]`. +- **Four runs, four hashes.** The committed S01b B1a documents for four real Both runs of that one model carry four + different `canonicalModelHash` values: `6d455480…`, `fcb0cd9b…`, `a3df983c…` and `5e56751e…`. +- **Cause.** `AnalyticalWindow.SetDefaultViewSettings` runs on `Opened`. A model with no views gets + `UI.Query.DefaultViewSettings`, whose GUID is `Guid.NewGuid()`. +- **What the parameter is.** SAM_UI's view state: views, visibility, cameras and legends. It never reaches a simulation. + SAM already excludes it from `SimulationResultProvenance.Fingerprint` for the same reason. +- **Found while fixing it, with the real producers.** Once that parameter is removed, its set is left empty, and the + empty set still carries a GUID: `Core.Query.Guid(assembly)`, the SAM.Analytical.UI module version id. That id changes with + every SAM_UI build. The test build had `afeedf2b…` and the S01b app build `29968788…`, so two builds still disagreed. + +## Change +- **`Query/SimulationSnapshot.cs` (new):** `Query.SimulationSnapshotJsonObject(model)` takes the model's own freshly built + JSON tree and changes only that tree, never the model: + - it removes the `UI Geometry Settings` parameter; + - it removes the SAM.Analytical.UI parameter set only if that leaves it empty. Anything else in that set stays, as does + every other parameter set, GUID and piece of metadata. +- **`Modify.FreezeAnalyticalModel`:** writes that JSON for the one snapshot both routes use: + - Run Simulation > Both: the file both producers hash (`sourceFileHash`, `canonicalModelHash`) and simulate; + - OpenStudio Simulation: the frozen copy is read back from it and converted. + + A `.json` snapshot is written exactly as `Core.Convert.ToFile` writes JSON: a one-element array, with + `Core.Convert.SerializerOptions(Indented)`. A model with no view state is therefore frozen byte for byte as before. Other + file types go through `Core.Convert.ToFile`, as before. +- **`Query.ViewState` / `Modify.WithViewState`:** the OpenStudio result model is built from the frozen copy. + - A copy of the open model's view state is taken at the moment of the snapshot, on the run's thread, as the snapshot + itself is. + - It is attached to the result model before saving, so the result model opens with the views it had before S04. + - Both steps are inside try/catch. A failure only adds a warning to the run's diagnostics and never changes the outcome. +- **Unchanged:** + - geometry, constructions, schedules, weather, design days, simulation options and native-model conversion; + - the producers, SAM_Validation and its `canonicalizationVersion`; + - the manifests and their schemas; + - the Tas-only route, which writes no snapshot. + +## Decisions and assumptions +- **Fix the input, not the hash rule.** The canonical hash is computed by the producers (SAM_Tas, SAM_OpenStudio) from the + snapshot SAM_UI writes, using SAM_Validation's generic canonical JSON v1. Excluding a SAM_UI parameter inside that generic + rule would be a canonicalization change across three more repositories. Keeping UI-only state out of the engine input is + the smallest correction, and it also stabilises `sourceFileHash` and the snapshot SHA-256. +- **Deny one named parameter, not an allow list.** This is the same name and the same reasoning as SAM's Fingerprint + exclusion. A parameter added later that bears on a simulation is kept automatically. +- **The emptied set is dropped, a non-empty one is kept.** An empty set carries only its per-build GUID. A set holding + anything else is left alone: removing it would be indiscriminate. +- **Historical documents keep their hashes.** Earlier B1a documents and manifests keep their session-dependent hashes. A new + snapshot of the same model has a canonical hash that differs from every pre-S04 one, just as pre-S04 hashes already + differed from session to session. `canonicalizationVersion` is unchanged because the algorithm is unchanged; only the + input lost its UI-only state. + +## Tests +New `CanonicalSnapshotTests.cs`: 14 tests in the WPF collection, on the Part O multizone fixture (2 flats plus a corridor, +with spaces, panels, apertures, constructions, internal conditions and weather). +- **Method:** models are saved, then reopened as the window opens them: `UIAnalyticalModel.Open`, then the window's + default-view step using the production `UI.Query.DefaultViewSettings`. The canonical hash is computed as the producers + compute it: the snapshot is loaded into an AnalyticalModel, then canonical JSON v1 is taken of its JSON. +- **A:** the same saved model opened twice. The opened models' view GUIDs differ (the F7 precondition). The snapshot + SHA-256 and the canonical hash are equal. +- **B:** different UI-only view state (another view, new GUIDs) gives the same canonical hash. The view state's set with + another build's GUID also gives the same snapshot and hash. +- **C (theory):** each of these changes the hash: + - a construction; + - a space parameter; + - the weather; + - a model parameter; + - another parameter in the SAM.Analytical.UI set (only the named view state is dropped). +- **Snapshot is the source less exactly the view state:** its JSON equals the source's JSON with only the parameter and its + emptied set removed. A model with no view state is frozen byte for byte as `Core.Convert.ToFile` freezes it. +- **D/E:** two Run Simulation > Both runs of the model, opened in two sessions, feed their producers byte-identical + snapshots in different run folders. Each manifest's `snapshot.sha256` is its own file's hash, and both producers of a run + read that one file. +- **F:** `WithViewState` gives the frozen result model a copy (not the instance) of the source's view state. + - The existing `OpenStudioSimulationTests.RealRun_…` now gives the model a view state. It checks that the real snapshot has + none and the real result model has it, compared after the JSON round trip the saved file makes. + - That test runs only where OpenStudio is installed. +- **G:** freezing leaves the source model byte-identical, and the source keeps its view state. +- Existing snapshot consumers pass unchanged: `EngineComparisonTests`, `OpenStudioSimulationTests`, + `TasFailureReportingTests`, `SimulationConcurrencyGuardTests`. + +## Validation +Local results on this laptop. Dependencies: SAM `62b54156`, SAM_Tas `e84aeff6`, SAM_OpenStudio `d4216f3` and +SAM_Validation `68eded0`, all on `sow/2026-Q4` and clean. +- `dotnet build WPF/SAM.Analytical.UI.WPF.Tests -c Release`: 0 errors. +- Focused (`CanonicalSnapshotTests|OpenStudioSimulationTests|EngineComparisonTests|WpfCollectionTests`): + **45/45 PASS** on `29ca1d1a`, including the real OpenStudio run. +- **Mutation checks on `0594e708`** (each reverted), all caught: + + | Mutation | Tests failed | + |---|---| + | Snapshot written as before S04 | 7 | + | Every SAM.Analytical.UI set dropped (over-removal) | 1 | + | Emptied set kept | 2 | + | Result model not given its view state back | 1 (the real OpenStudio run) | + + An earlier variant was also caught: removing the weather too failed 2 tests, on `1f24f8af`. +- Full `SAM.Analytical.UI.WPF.Tests` (real `APPDATA`): `0594e708` **2873/2873 PASS**; after the review fixes `29ca1d1a` **2874/2874 PASS** (2860 + 14 new). + +## Independent review (2026-10-19) and fixes in `29ca1d1a` +A separate regression-minded review of the combined S03+S04 diff confirmed two S04 defects, both fixed: +1. **Medium.** `WithViewState` read the live open model at the **end** of the OpenStudio run, on the worker thread, with no + guard. A collection change during the run could throw and turn a successful run into Failed, and it would have picked up + view edits made while the run was going. + - The view state is now copied at freeze time and attached inside a try/catch. + - The test now edits the source after the freeze and proves that the edit does not reach the result model. +2. **Low.** An already-empty SAM.Analytical.UI set, written with no `"Parameters"` key, was kept along with its per-build + GUID. + - It is now dropped, like an emptied set. + - New test: such a model freezes byte for byte as one that never had the set. Mutation check: keeping the set → 1 fails. + +The review also confirmed: +- the live model is never mutated, because `ToJsonObject` deep-clones parameter sets; +- `.json` snapshots are byte-identical to `Core.Convert.ToFile`; +- a missing directory returns null, as before; +- duplicate same-name sets are all processed. + +## Evidence (real producers, licensed laptop) +Evidence is in `documentation/evidence/s04-20261019/`: `real-producers.txt`, plus a manifest and both B1a documents for each +session. The harness is `tools/S04EvidenceHarness.cs.txt`; it is not compiled. + +**E1: two sessions, real producers.** The model is opened afresh as the window opens it, and run through real Both, +benchmark-tas and benchmark-openstudio. + +| | Session 1 | Session 2 | +|---|---|---| +| View GUID | `cc6f50a9…` | `d72e130c…` | +| Snapshot | `0c30db14…` | `0c30db14…` | +| `sourceFileHash` | `0c30db14…` | `0c30db14…` | +| `canonicalModelHash`, both producers | `05c1e334…` | `05c1e334…` | +| Status | Compared | Compared | + +**E2: the four historical S01b snapshots, refrozen under S04.** These were made by two different app builds, and before +S04 they had four canonical hashes (`6d455480…`, `fcb0cd9b…`, `a3df983c…`, `5e56751e…`). All four now give snapshot +`0c30db14…` and canonical hash `05c1e334…`, the same values as E1, which came from the test build. + +## Integration with S03 (2026-10-20) +- `3bbba54b` merges `sow/2026-Q4` `56e67d09` (S03 SAM_UI#242 merged as `1e460bbe`, plus its docs-only closeout) into + `b19b80ab`. The one conflict, in `OpenStudioSimulationTests.RealRun_…`, keeps both the S04 view-state block and the S03 + provenance block. The resolved file and every code file equal the local integration tree `8aafcb06`; the only difference + from `8aafcb06` is `PROJECT_PROGRESS.md` (the S03 closeout, from the base). +- S04 behaviour kept, checked in the merged tree: the snapshot drops only `UI Geometry Settings` and an emptied or empty + SAM.Analytical.UI set (`Query/SimulationSnapshot.cs`); every other parameter set, model parameter and the weather stay; + the view state is copied at freeze time and attached to the result model in a try/catch. +- Full `dotnet test WPF/SAM.Analytical.UI.WPF.Tests -c Release` on `3bbba54b` (real `APPDATA`; dependency builds as above, + local, machine-local logs `C:\TasOut\s05\logs\fulltest-3bbba54b*.txt`): + - run 1: **2925/2926**. `TasRunManifestTests.Succeeded_RecordsTheWorkflowSettingsThatRan_AndTheEmbeddedWeather` (S03) + reported `Cancelled`, after exactly 5 s. Cause: under full-suite load, `ProgressWindowHost` did not confirm its dialog + thread's shutdown within its 5 s join, and `Simulate` then treats the run as possibly cancelled by design + (`!progressWindowHost.ShutdownCompleted`, unchanged since 2026-10-06). Timing, not a merge or S04 effect. + - run 2, same build: **2926/2926 PASS**. + - Recorded, not fixed here: on a heavily loaded machine an ordinary Tas run can be reported Cancelled when the progress + window's thread is slow to stop. Pre-existing product behaviour, outside S04. +- **Local dependency caveat (found 2026-10-20):** the machine's `SAM\build` predates SAM#212 (F1), so these local runs used + a pre-F1 `SAM.Weather.dll`; hosted CI builds the dependencies from source and is the authoritative check. S05 acceptance + rebuilds the dependency stack from the `sow/2026-Q4` tips. + +## Unresolved, risks +- `UIGeometrySettings` does not round-trip its default view's appearance dictionary through JSON, because its keys are + Guids. This is pre-existing, behaves the same before and after S04, and was found while writing the result-model test. +- No licensed real-UI run has been done; that is roadmap S05. +- The Both runs in the evidence needed `SAM_COMMIT` for the developer-built producers. See S03 (SAM_UI#242). + +## Next step +Merge once the hosted Build and SPDX checks pass at the final head (owner authorisation 2026-10-20). After merge, add +the post-merge `PROJECT_PROGRESS.md` closeout on `sow/2026-Q4`; S05 formal acceptance follows on the merged content. diff --git a/documentation/evidence/s04-20261019/README.md b/documentation/evidence/s04-20261019/README.md new file mode 100644 index 00000000..34ce3f94 --- /dev/null +++ b/documentation/evidence/s04-20261019/README.md @@ -0,0 +1,15 @@ +# S04 stable canonical snapshot - real-producer evidence (2026-10-19) + +Licensed development laptop. SAM_UI S04 content `0594e708`. Producers are developer builds: SAM_Tas `e84aeff6` and +SAM_OpenStudio `d4216f3`, with `SAM_COMMIT` set by hand (see S03, SAM_UI#242). The model is `box-aperture.json` and the +weather `Budapest.epw` (machine-local, `C:\TasOut\s01b\inputs`). The SAM_UI code paths were driven by +`tools/S04EvidenceHarness.cs.txt`, a temporary xUnit harness that is not compiled, and not by the ribbon. + +- `real-producers.txt`: the output of the harness. + - **E1:** two Both runs, each on the model opened afresh as the window opens it, with different view GUIDs. Both runs + gave snapshot `0c30db14…`, and both real producers reported `canonicalModelHash` `05c1e334…`. + - **E2:** the four historical S01b snapshots were frozen again under S04. They came from two app builds and had four + pre-S04 canonical hashes. All four now give the same snapshot and hash as E1. +- `session-1/` and `session-2/`: each run's `engine-comparison-run.json` and both B1a documents. + +Paths inside the files are machine-local. diff --git a/documentation/evidence/s04-20261019/real-producers.txt b/documentation/evidence/s04-20261019/real-producers.txt new file mode 100644 index 00000000..a5185c51 --- /dev/null +++ b/documentation/evidence/s04-20261019/real-producers.txt @@ -0,0 +1,12 @@ +Passed SAM.Analytical.UI.WPF.Tests.S04EvidenceHarness.E1_TwoRealBothRuns_TwoSessions [58 s] +session-1: status=Compared view=cc6f50a9-8d0e-49de-8a94-c404fbee7374 snapshot=sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +Tas: exit=0 canonicalModelHash=sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500 sourceFileHash=sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +OpenStudio: exit=0 canonicalModelHash=sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500 sourceFileHash=sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +session-2: status=Compared view=d72e130c-1306-4b66-b297-8c365e069134 snapshot=sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +Tas: exit=0 canonicalModelHash=sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500 sourceFileHash=sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +OpenStudio: exit=0 canonicalModelHash=sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500 sourceFileHash=sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +Passed SAM.Analytical.UI.WPF.Tests.S04EvidenceHarness.E2_HistoricalSnapshots_RefrozenUnderS04 [2 s] +B0-base-both-tas-refused\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-183629_fba4e40f\snapshot.json: pre-S04 canonical sha256:6d455480d148b86d2c3a452255d451d06244ce84e2db01f2ef0d3102fd15fe51 -> S04 canonical sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500, snapshot sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +B1-cand-both-tas-refused\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-183527_70cca5f6\snapshot.json: pre-S04 canonical sha256:fcb0cd9bcdabb33ded9aaf0f9a3b8d0d50005025dd2959068b03682d0efb3940 -> S04 canonical sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500, snapshot sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +B2-cand-both-success\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-183722_2f78f5e3\snapshot.json: pre-S04 canonical sha256:a3df983c3d8d7a83c5059c2146d0649ba593f88d0a1e4a37d22dbd270f354835 -> S04 canonical sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500, snapshot sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 +B3-base-both-success\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-183816_0afec0ff\snapshot.json: pre-S04 canonical sha256:5e56751ee754eed85c0ad2844562a37e2337a19e556206ff80bae2744f947679 -> S04 canonical sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500, snapshot sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05 diff --git a/documentation/evidence/s04-20261019/session-1/benchmark-OpenStudio.json b/documentation/evidence/s04-20261019/session-1/benchmark-OpenStudio.json new file mode 100644 index 00000000..961b8352 --- /dev/null +++ b/documentation/evidence/s04-20261019/session-1/benchmark-OpenStudio.json @@ -0,0 +1,140 @@ +{ + "schemaVersion": "1.0.0", + "provenance": { + "sourceModelName": "000000_SAM_AnalyticalModel", + "sourceModelGuid": "1988f7b93ec047d0b87bf9cb345d9eab", + "sourceFileHash": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05", + "canonicalModelHash": "sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500", + "canonicalizationVersion": "1.0.0", + "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", + "runTimestampUtc": "2026-10-19T21:01:17.1338121Z", + "durationSeconds": 5.586659, + "state": "Success", + "resultSources": [ + "SAM.Analytical.OpenStudio" + ], + "warnings": [ + "[SAM-OS-IC-001] Warning: Pollutant generation is not converted (EnergyPlus generic contaminant modelling is out of scope) (SAM InternalCondition 9e54af09-65fb-4f64-98d7-200db3be2457) \u2192 SAM_InternalCondition_S39_OfficeOpen_30b69a1f" + ], + "notes": [ + "[SAM-OS-RUN-002] Information: Annual weather source: explicit EPW (weather.epw)", + "[SAM-OS-RUN-002] Information: Ground temperatures taken from the SAM model WeatherData (depth 0.5 m set, 12 monthly values)", + "[SAM-OS-RUN-002] Information: No design-day source supplied; sizing-period runs stay disabled unless OpenStudioConversionOptions.RunSizingPeriods overrides" + ] + }, + "model": { + "consumptionHeating": { + "value": 2173.9185033546732, + "unit": "kWh", + "available": true + }, + "consumptionCooling": { + "value": 4556.226212241192, + "unit": "kWh", + "available": true + }, + "peakHeatingLoad": { + "value": 3.2591834042717536, + "unit": "kW", + "available": true + }, + "peakHeatingHour": { + "value": 8455, + "unit": "hourOfYear", + "available": true + }, + "peakCoolingLoad": { + "value": 5.332620570657522, + "unit": "kW", + "available": true + }, + "peakCoolingHour": { + "value": 5029, + "unit": "hourOfYear", + "available": true + }, + "floorArea": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + } + }, + "spaces": [ + { + "guid": "cb68bed195af496bb255a13ffa5c58e9", + "name": "Office", + "area": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + }, + "heating": { + "designLoad": { + "value": null, + "unit": "W", + "available": false + }, + "peakLoad": { + "value": 3259.1834042717537, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 8455, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + }, + "cooling": { + "designLoad": { + "value": null, + "unit": "W", + "available": false + }, + "peakLoad": { + "value": 5332.620570657522, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 5029, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + } + } + ] +} \ No newline at end of file diff --git a/documentation/evidence/s04-20261019/session-1/benchmark-TAS.json b/documentation/evidence/s04-20261019/session-1/benchmark-TAS.json new file mode 100644 index 00000000..bb9c77ab --- /dev/null +++ b/documentation/evidence/s04-20261019/session-1/benchmark-TAS.json @@ -0,0 +1,136 @@ +{ + "schemaVersion": "1.0.0", + "provenance": { + "sourceModelName": "000000_SAM_AnalyticalModel", + "sourceModelGuid": "1988f7b93ec047d0b87bf9cb345d9eab", + "sourceFileHash": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05", + "canonicalModelHash": "sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500", + "canonicalizationVersion": "1.0.0", + "samCommit": "62b5415697d1a5af143c6b581b7d42fbd6551ca8", + "runnerCommit": "e84aeff687033354e3f17891635fdf10d42ad68f", + "engine": { + "kind": "TAS", + "name": "Tas", + "version": "9.5.7.0", + "sdkVersion": null + }, + "route": "Native-TAS", + "weather": { + "identity": "weather", + "hash": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3" + }, + "designDaySource": "None", + "runTimestampUtc": "2026-10-19T21:00:56.8285942Z", + "durationSeconds": 18.3678412, + "state": "Success", + "resultSources": [ + "SAM.Analytical.Tas" + ], + "warnings": [], + "notes": [ + "Shared gbXML was exported from the source model (no --gbxml supplied)." + ] + }, + "model": { + "consumptionHeating": { + "value": 220.62682055425643, + "unit": "kWh", + "available": true + }, + "consumptionCooling": { + "value": 5540.314465834558, + "unit": "kWh", + "available": true + }, + "peakHeatingLoad": { + "value": 1.4238231201171876, + "unit": "kW", + "available": true + }, + "peakHeatingHour": { + "value": 775, + "unit": "hourOfYear", + "available": true + }, + "peakCoolingLoad": { + "value": 3.126279052734375, + "unit": "kW", + "available": true + }, + "peakCoolingHour": { + "value": 3901, + "unit": "hourOfYear", + "available": true + }, + "floorArea": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + } + }, + "spaces": [ + { + "guid": "cb68bed195af496bb255a13ffa5c58e9", + "name": "Office", + "area": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + }, + "heating": { + "designLoad": { + "value": 1927.0106201171875, + "unit": "W", + "available": true + }, + "peakLoad": { + "value": 1926.94873046875, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 936, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + }, + "cooling": { + "designLoad": { + "value": 3415.618896484375, + "unit": "W", + "available": true + }, + "peakLoad": { + "value": 3415.45556640625, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 4358, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + } + } + ] +} \ No newline at end of file diff --git a/documentation/evidence/s04-20261019/session-1/engine-comparison-run.json b/documentation/evidence/s04-20261019/session-1/engine-comparison-run.json new file mode 100644 index 00000000..023c929a --- /dev/null +++ b/documentation/evidence/s04-20261019/session-1/engine-comparison-run.json @@ -0,0 +1,68 @@ +{ + "schema": "sam-ui.engine-comparison-run/1", + "id": "5abf6f80", + "status": "Compared", + "startedUtc": "2026-10-19T21:00:55.6685544Z", + "finishedUtc": "2026-10-19T21:01:24.6641845Z", + "sourcePath": "C:\\TasOut\\s04\\box-aperture.json", + "modelName": "000000_SAM_AnalyticalModel", + "snapshot": { + "path": "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\snapshot.json", + "sha256": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05" + }, + "weather": { + "source": "File", + "path": "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\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\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\snapshot.json", + "--weather", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\weather.epw", + "--tbd", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\tas\\000000_SAM_AnalyticalModel.tbd", + "--out", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\benchmark-TAS.json" + ], + "exitCode": 0, + "durationSeconds": 20.287, + "log": "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\benchmark-tas.log", + "benchmark": "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\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\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\snapshot.json", + "--weather", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\weather.epw", + "--out", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\benchmark-OpenStudio.json", + "--work", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\openstudio" + ], + "exitCode": 0, + "durationSeconds": 8.139, + "log": "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\benchmark-openstudio.log", + "benchmark": "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\benchmark-OpenStudio.json", + "error": null, + "succeeded": true + } + ], + "gate": "Fail", + "reports": [ + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\comparison.md", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\comparison.csv", + "C:\\TasOut\\s04\\session-1\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210055_5abf6f80\\comparison-summary.json" + ], + "diagnostics": [], + "error": null +} \ No newline at end of file diff --git a/documentation/evidence/s04-20261019/session-2/benchmark-OpenStudio.json b/documentation/evidence/s04-20261019/session-2/benchmark-OpenStudio.json new file mode 100644 index 00000000..0ba008b3 --- /dev/null +++ b/documentation/evidence/s04-20261019/session-2/benchmark-OpenStudio.json @@ -0,0 +1,140 @@ +{ + "schemaVersion": "1.0.0", + "provenance": { + "sourceModelName": "000000_SAM_AnalyticalModel", + "sourceModelGuid": "1988f7b93ec047d0b87bf9cb345d9eab", + "sourceFileHash": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05", + "canonicalModelHash": "sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500", + "canonicalizationVersion": "1.0.0", + "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", + "runTimestampUtc": "2026-10-19T21:01:46.7457631Z", + "durationSeconds": 5.3043344, + "state": "Success", + "resultSources": [ + "SAM.Analytical.OpenStudio" + ], + "warnings": [ + "[SAM-OS-IC-001] Warning: Pollutant generation is not converted (EnergyPlus generic contaminant modelling is out of scope) (SAM InternalCondition 9e54af09-65fb-4f64-98d7-200db3be2457) \u2192 SAM_InternalCondition_S39_OfficeOpen_30b69a1f" + ], + "notes": [ + "[SAM-OS-RUN-002] Information: Annual weather source: explicit EPW (weather.epw)", + "[SAM-OS-RUN-002] Information: Ground temperatures taken from the SAM model WeatherData (depth 0.5 m set, 12 monthly values)", + "[SAM-OS-RUN-002] Information: No design-day source supplied; sizing-period runs stay disabled unless OpenStudioConversionOptions.RunSizingPeriods overrides" + ] + }, + "model": { + "consumptionHeating": { + "value": 2173.9185033546732, + "unit": "kWh", + "available": true + }, + "consumptionCooling": { + "value": 4556.226212241192, + "unit": "kWh", + "available": true + }, + "peakHeatingLoad": { + "value": 3.2591834042717536, + "unit": "kW", + "available": true + }, + "peakHeatingHour": { + "value": 8455, + "unit": "hourOfYear", + "available": true + }, + "peakCoolingLoad": { + "value": 5.332620570657522, + "unit": "kW", + "available": true + }, + "peakCoolingHour": { + "value": 5029, + "unit": "hourOfYear", + "available": true + }, + "floorArea": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + } + }, + "spaces": [ + { + "guid": "cb68bed195af496bb255a13ffa5c58e9", + "name": "Office", + "area": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + }, + "heating": { + "designLoad": { + "value": null, + "unit": "W", + "available": false + }, + "peakLoad": { + "value": 3259.1834042717537, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 8455, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + }, + "cooling": { + "designLoad": { + "value": null, + "unit": "W", + "available": false + }, + "peakLoad": { + "value": 5332.620570657522, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 5029, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + } + } + ] +} \ No newline at end of file diff --git a/documentation/evidence/s04-20261019/session-2/benchmark-TAS.json b/documentation/evidence/s04-20261019/session-2/benchmark-TAS.json new file mode 100644 index 00000000..c2a1b381 --- /dev/null +++ b/documentation/evidence/s04-20261019/session-2/benchmark-TAS.json @@ -0,0 +1,136 @@ +{ + "schemaVersion": "1.0.0", + "provenance": { + "sourceModelName": "000000_SAM_AnalyticalModel", + "sourceModelGuid": "1988f7b93ec047d0b87bf9cb345d9eab", + "sourceFileHash": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05", + "canonicalModelHash": "sha256:05c1e334af00f339d36d5b97828692d3d7d27269d2168a945dfc4a35919c9500", + "canonicalizationVersion": "1.0.0", + "samCommit": "62b5415697d1a5af143c6b581b7d42fbd6551ca8", + "runnerCommit": "e84aeff687033354e3f17891635fdf10d42ad68f", + "engine": { + "kind": "TAS", + "name": "Tas", + "version": "9.5.7.0", + "sdkVersion": null + }, + "route": "Native-TAS", + "weather": { + "identity": "weather", + "hash": "sha256:472ffb4e2f10867600949a392ce46891fc1bd05f2fc8d8917a99bca7d26209c3" + }, + "designDaySource": "None", + "runTimestampUtc": "2026-10-19T21:01:25.8629255Z", + "durationSeconds": 18.7940658, + "state": "Success", + "resultSources": [ + "SAM.Analytical.Tas" + ], + "warnings": [], + "notes": [ + "Shared gbXML was exported from the source model (no --gbxml supplied)." + ] + }, + "model": { + "consumptionHeating": { + "value": 220.62682055425643, + "unit": "kWh", + "available": true + }, + "consumptionCooling": { + "value": 5540.314465834558, + "unit": "kWh", + "available": true + }, + "peakHeatingLoad": { + "value": 1.4238231201171876, + "unit": "kW", + "available": true + }, + "peakHeatingHour": { + "value": 775, + "unit": "hourOfYear", + "available": true + }, + "peakCoolingLoad": { + "value": 3.126279052734375, + "unit": "kW", + "available": true + }, + "peakCoolingHour": { + "value": 3901, + "unit": "hourOfYear", + "available": true + }, + "floorArea": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + } + }, + "spaces": [ + { + "guid": "cb68bed195af496bb255a13ffa5c58e9", + "name": "Office", + "area": { + "value": 50, + "unit": "m2", + "available": true + }, + "volume": { + "value": 150, + "unit": "m3", + "available": true + }, + "heating": { + "designLoad": { + "value": 1927.0106201171875, + "unit": "W", + "available": true + }, + "peakLoad": { + "value": 1926.94873046875, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 936, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + }, + "cooling": { + "designLoad": { + "value": 3415.618896484375, + "unit": "W", + "available": true + }, + "peakLoad": { + "value": 3415.45556640625, + "unit": "W", + "available": true + }, + "peakHour": { + "value": 4358, + "unit": "hourOfYear", + "available": true + }, + "unmetHours": { + "value": 0, + "unit": "h", + "available": true + } + } + } + ] +} \ No newline at end of file diff --git a/documentation/evidence/s04-20261019/session-2/engine-comparison-run.json b/documentation/evidence/s04-20261019/session-2/engine-comparison-run.json new file mode 100644 index 00000000..570da4cb --- /dev/null +++ b/documentation/evidence/s04-20261019/session-2/engine-comparison-run.json @@ -0,0 +1,68 @@ +{ + "schema": "sam-ui.engine-comparison-run/1", + "id": "ab36d659", + "status": "Compared", + "startedUtc": "2026-10-19T21:01:24.7556487Z", + "finishedUtc": "2026-10-19T21:01:53.9613744Z", + "sourcePath": "C:\\TasOut\\s04\\box-aperture.json", + "modelName": "000000_SAM_AnalyticalModel", + "snapshot": { + "path": "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\snapshot.json", + "sha256": "sha256:0c30db14a9b10f5e651248e70c942bf875ca93dcee9436998b9960e47c0a6d05" + }, + "weather": { + "source": "File", + "path": "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\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\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\snapshot.json", + "--weather", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\weather.epw", + "--tbd", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\tas\\000000_SAM_AnalyticalModel.tbd", + "--out", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\benchmark-TAS.json" + ], + "exitCode": 0, + "durationSeconds": 20.809, + "log": "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\benchmark-tas.log", + "benchmark": "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\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\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\snapshot.json", + "--weather", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\weather.epw", + "--out", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\benchmark-OpenStudio.json", + "--work", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\openstudio" + ], + "exitCode": 0, + "durationSeconds": 8.045, + "log": "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\benchmark-openstudio.log", + "benchmark": "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\benchmark-OpenStudio.json", + "error": null, + "succeeded": true + } + ], + "gate": "Fail", + "reports": [ + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\comparison.md", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\comparison.csv", + "C:\\TasOut\\s04\\session-2\\000000_SAM_AnalyticalModel_TasOpenStudio_20261019-210124_ab36d659\\comparison-summary.json" + ], + "diagnostics": [], + "error": null +} \ No newline at end of file diff --git a/documentation/evidence/s04-20261019/tools/S04EvidenceHarness.cs.txt b/documentation/evidence/s04-20261019/tools/S04EvidenceHarness.cs.txt new file mode 100644 index 00000000..311ea28b --- /dev/null +++ b/documentation/evidence/s04-20261019/tools/S04EvidenceHarness.cs.txt @@ -0,0 +1,108 @@ +// Temporary, NOT committed: S04 evidence with real producers on a licensed machine. +#nullable enable + +using SAM.Analytical.Benchmark; +using SAM.Core; +using SAM.Geometry.UI; +using System; +using System.IO; +using System.Linq; +using System.Text.Json.Nodes; +using Xunit; +using Xunit.Abstractions; + +namespace SAM.Analytical.UI.WPF.Tests +{ + [Collection(WpfCollection.Name)] + public class S04EvidenceHarness + { + private const string Inputs = @"C:\TasOut\s01b\inputs"; + private const string Root = @"C:\TasOut\s04"; + private readonly ITestOutputHelper output; + + public S04EvidenceHarness(ITestOutputHelper output) { this.output = output; } + + private static bool Enabled => Environment.GetEnvironmentVariable("S04_EVIDENCE") == "1"; + + /// Two real Both runs, each on the model opened afresh as the window opens it. + [WpfFact] + public void E1_TwoRealBothRuns_TwoSessions() + { + if (!Enabled) return; + Directory.CreateDirectory(Root); + string modelPath = Path.Combine(Root, "box-aperture.json"); + File.Copy(Path.Combine(Inputs, "box-aperture.json"), modelPath, true); + + string? samCommit_Before = Environment.GetEnvironmentVariable("SAM_COMMIT"); + string? path_Before = Environment.GetEnvironmentVariable("PATH"); + Environment.SetEnvironmentVariable("SAM_COMMIT", "62b5415697d1a5af143c6b581b7d42fbd6551ca8"); + Environment.SetEnvironmentVariable("PATH", @"C:\Users\michal\Documents\GitHub\SAM-BIM\.icon-migration\runtime\OpenStudio-3.10.0+86d7e215a1-Windows\bin;" + path_Before); + try + { + foreach (string session in new[] { "session-1", "session-2" }) + { + AnalyticalModel analyticalModel = OpenAsTheWindowDoes(modelPath); + EngineComparisonRun run = analyticalModel.RunEngineComparison(Path.Combine(Root, session), 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", + modelPath); + output.WriteLine("{0}: status={1} view={2} snapshot={3}", session, run.Status, ViewGuid(analyticalModel), run.SnapshotSha256); + foreach (EngineComparisonProducerRun producerRun in run.ProducerRuns) + { + JsonNode? provenance = producerRun.BenchmarkPath == null ? null : JsonNode.Parse(File.ReadAllText(producerRun.BenchmarkPath))?["provenance"]; + output.WriteLine(" {0}: exit={1} canonicalModelHash={2} sourceFileHash={3}", producerRun.Engine, producerRun.ExitCode, provenance?["canonicalModelHash"], provenance?["sourceFileHash"]); + } + } + } + finally + { + Environment.SetEnvironmentVariable("SAM_COMMIT", samCommit_Before); + Environment.SetEnvironmentVariable("PATH", path_Before); + } + } + + /// The four historical S01b snapshots (pre-S04, four different canonical hashes) frozen again under S04. + [WpfFact] + public void E2_HistoricalSnapshots_RefrozenUnderS04() + { + if (!Enabled) return; + Directory.CreateDirectory(Root); + foreach (string path in Directory.GetFiles(@"C:\TasOut\s01b\runs", "snapshot.json", SearchOption.AllDirectories).OrderBy(x => x)) + { + AnalyticalModel analyticalModel = Core.Convert.ToSAM(path).First(); + string before = BenchmarkCanonicalJson.ComputeSha256(analyticalModel.ToJsonObject().ToJsonString()); + string path_Refrozen = Path.Combine(Root, "refrozen-" + Path.GetFileName(Path.GetDirectoryName(Path.GetDirectoryName(path))!) + ".json"); + AnalyticalModel? refrozen = analyticalModel.FreezeAnalyticalModel(path_Refrozen); + string after = BenchmarkCanonicalJson.ComputeSha256(Core.Convert.ToSAM(path_Refrozen).First().ToJsonObject().ToJsonString()); + output.WriteLine("{0}: pre-S04 canonical {1} -> S04 canonical {2}, snapshot {3}", path.Replace(@"C:\TasOut\s01b\runs\", ""), before, after, Query.Sha256(path_Refrozen)); + } + } + + private static AnalyticalModel OpenAsTheWindowDoes(string path) + { + UIAnalyticalModel uIAnalyticalModel = new UIAnalyticalModel(path); + Assert.True(uIAnalyticalModel.Open()); + AnalyticalModel analyticalModel = uIAnalyticalModel.JSAMObject; + if (!analyticalModel.TryGetValue(AnalyticalModelParameter.UIGeometrySettings, out UIGeometrySettings uIGeometrySettings) || uIGeometrySettings == null) + { + uIGeometrySettings = new UIGeometrySettings(); + } + + if (uIGeometrySettings.GetViewSettings() is not { Count: > 0 }) + { + ViewSettings viewSettings = UI.Query.DefaultViewSettings(analyticalModel); + uIGeometrySettings.AddViewSettings(viewSettings); + uIGeometrySettings.ActiveGuid = viewSettings.Guid; + analyticalModel.SetValue(AnalyticalModelParameter.UIGeometrySettings, uIGeometrySettings); + } + + return analyticalModel; + } + + private static string ViewGuid(AnalyticalModel analyticalModel) + { + analyticalModel.TryGetValue(AnalyticalModelParameter.UIGeometrySettings, out UIGeometrySettings uIGeometrySettings); + return uIGeometrySettings?.ActiveGuid.ToString() ?? "(none)"; + } + } +}