From 1f24f8af8a67a7d2a09420ebf52030b1ebaa28c0 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Mon, 19 Oct 2026 22:43:44 +0200 Subject: [PATCH 1/8] fix(ui): stable canonical simulation snapshot (S04, F7) The frozen snapshot that Run Simulation > Both's producers hash and OpenStudio Simulation converts now leaves out the one UI-only parameter SAM_UI keeps on a model, "UI Geometry Settings" (its views, regenerated with a new GUID every time a model is opened). The same saved model opened in two sessions therefore gives byte-identical snapshots and the same canonical model hash, while every engineering edit still changes it. Nothing else is removed; the open model is never changed; the OpenStudio result model gets the open model's view state back, as before. Co-Authored-By: Claude Opus 5.5 --- .../CanonicalSnapshotTests.cs | 314 ++++++++++++++++++ .../Modify/RunOpenStudioSimulation.cs | 13 +- .../Query/SimulationSnapshot.cs | 55 +++ 3 files changed, 379 insertions(+), 3 deletions(-) create mode 100644 WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs create mode 100644 WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs 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..6fd440f5 --- /dev/null +++ b/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs @@ -0,0 +1,314 @@ +// 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")); + + JsonObject expected = analyticalModel.ToJsonObject(); + JsonObject uiParameterSet = expected["ParameterSets"]!.AsArray().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")); + + 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)); + } + + /// 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)); + + AnalyticalModel analyticalModel_Result = Modify.WithViewState(analyticalModel_Frozen, analyticalModel); + + 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/Modify/RunOpenStudioSimulation.cs b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs index e25ea7d9..de8c1836 100644 --- a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs +++ b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs @@ -76,7 +76,7 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod cancellationToken.ThrowIfCancellationRequested(); - RunOpenStudioSimulation_Run(result, analyticalModel_Frozen, epwPath, progress, cancellationToken); + RunOpenStudioSimulation_Run(result, analyticalModel_Frozen, analyticalModel, epwPath, progress, cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -99,6 +99,10 @@ 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. + /// /// public static AnalyticalModel? FreezeAnalyticalModel(this AnalyticalModel analyticalModel, string path) { @@ -107,7 +111,7 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod return null; } - if (!Core.Convert.ToFile(analyticalModel, path)) + if (!Core.Convert.ToFile(Query.SimulationSnapshot(analyticalModel), path)) { return null; } @@ -151,7 +155,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, AnalyticalModel analyticalModel_Source, string? epwPath, IProgress? progress, CancellationToken cancellationToken) { openStudioSimulationRun.SamOpenStudioVersion = typeof(OpenStudioConversionResult).Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(OpenStudioConversionResult).Assembly.GetName().Version?.ToString(); @@ -236,6 +240,9 @@ 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 it back. + WithViewState(analyticalModel_Result, analyticalModel_Source); + 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..7f0062d1 --- /dev/null +++ b/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +namespace SAM.Analytical.UI.WPF +{ + public static partial class Query + { + /// + /// The model as a frozen simulation snapshot holds it (roadmap S04, finding F7 of SAM_UI#239): + /// 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: everything else on the model, including anything else in the SAM.Analytical.UI parameter + /// set, stays in the snapshot. The same parameter is already left out of SimulationResultProvenance.Fingerprint in SAM. + /// + /// + /// The model given is never changed. When it carries the view state, the result is a copy (whose parameter sets are its + /// own clones) with the parameter removed; otherwise it is the model itself. + /// + /// + public static AnalyticalModel SimulationSnapshot(AnalyticalModel analyticalModel) + { + if (analyticalModel == null || !analyticalModel.HasValue(UI.AnalyticalModelParameter.UIGeometrySettings)) + { + return analyticalModel!; + } + + AnalyticalModel result = new AnalyticalModel(analyticalModel); + result.RemoveValue(UI.AnalyticalModelParameter.UIGeometrySettings); + return result; + } + } + + public static partial class Modify + { + /// + /// Gives - a model derived from a snapshot, such as the OpenStudio result model - a copy of the view + /// state of , 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, AnalyticalModel? analyticalModel_Source) + { + if (analyticalModel != null + && analyticalModel_Source != null + && analyticalModel_Source.TryGetValue(UI.AnalyticalModelParameter.UIGeometrySettings, out Geometry.UI.UIGeometrySettings uIGeometrySettings) + && uIGeometrySettings != null) + { + analyticalModel.SetValue(UI.AnalyticalModelParameter.UIGeometrySettings, new Geometry.UI.UIGeometrySettings(uIGeometrySettings)); + } + + return analyticalModel!; + } + } +} From 72c925651e033cb9fdd65db172f0bbbaae65615d Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Mon, 19 Oct 2026 22:46:03 +0200 Subject: [PATCH 2/8] test(ui): the real OpenStudio run keeps the view state off the snapshot and on the result model (S04) Co-Authored-By: Claude Opus 5.5 --- .../OpenStudioSimulationTests.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs index 7d63fe6f..07b7b846 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); @@ -257,6 +263,13 @@ public void RealRun_SimulatesAFrozenCopy_AndSavesAResultModelWithOnlyOpenStudioR Assert.NotEmpty(results); 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()); } } } From 0594e708abfff4936e2b026c42111f5e5ee83895 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Mon, 19 Oct 2026 22:58:27 +0200 Subject: [PATCH 3/8] fix(ui): drop the emptied UI parameter set from the snapshot (S04) Real producers showed the residual: once "UI Geometry Settings" is removed, the empty SAM.Analytical.UI parameter set still carries the assembly's module version id as its GUID, which changes with every SAM_UI build. The snapshot is now written from the model's JSON tree, less that parameter and less its set only when that leaves it empty, in exactly the format Core.Convert.ToFile writes (a model with no view state is frozen byte for byte as before). Co-Authored-By: Claude Opus 5.5 --- .../CanonicalSnapshotTests.cs | 44 ++++++++++++++- .../Modify/RunOpenStudioSimulation.cs | 19 +++++-- .../Query/SimulationSnapshot.cs | 54 ++++++++++++++----- 3 files changed, 101 insertions(+), 16 deletions(-) diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs index 6fd440f5..8d12bc06 100644 --- a/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs +++ b/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs @@ -119,16 +119,58 @@ 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(); - JsonObject uiParameterSet = expected["ParameterSets"]!.AsArray().Select(x => x!.AsObject()).Single(x => (string)x["Name"]! == "SAM.Analytical.UI"); + 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 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. diff --git a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs index de8c1836..efaaba43 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 @@ -100,18 +101,30 @@ 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 + /// 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; } - if (!Core.Convert.ToFile(Query.SimulationSnapshot(analyticalModel), path)) + JsonObject? jsonObject = Query.SimulationSnapshotJsonObject(analyticalModel); + if (jsonObject == null) + { + return null; + } + + 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; } diff --git a/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs b/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs index 7f0062d1..094b54a3 100644 --- a/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs +++ b/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs @@ -1,42 +1,72 @@ // 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 model as a frozen simulation snapshot holds it (roadmap S04, finding F7 of SAM_UI#239): - /// without UI Geometry Settings, the view state SAM_UI keeps on a model (its views, their visibility, cameras and legends). + /// 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: everything else on the model, including anything else in the SAM.Analytical.UI parameter - /// set, stays in the snapshot. The same parameter is already left out of SimulationResultProvenance.Fingerprint in SAM. + /// 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. When it carries the view state, the result is a copy (whose parameter sets are its - /// own clones) with the parameter removed; otherwise it is the model itself. + /// The model given is never changed: this edits the freshly built JSON tree, not the model. Null for no model. /// /// - public static AnalyticalModel SimulationSnapshot(AnalyticalModel analyticalModel) + public static JsonObject? SimulationSnapshotJsonObject(AnalyticalModel analyticalModel) { - if (analyticalModel == null || !analyticalModel.HasValue(UI.AnalyticalModelParameter.UIGeometrySettings)) + 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()) { - return analyticalModel!; + if (parameterSet["Parameters"] is not JsonArray parameters) + { + 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); + } } - AnalyticalModel result = new AnalyticalModel(analyticalModel); - result.RemoveValue(UI.AnalyticalModelParameter.UIGeometrySettings); return result; } + + private static string? Text(JsonNode? jsonNode) + { + return jsonNode is JsonValue jsonValue && jsonValue.TryGetValue(out string? value) ? value : null; + } } public static partial class Modify { /// /// Gives - a model derived from a snapshot, such as the OpenStudio result model - a copy of the view - /// state of , which left out of the snapshot. So a + /// state of , 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, AnalyticalModel? analyticalModel_Source) From bbc8a19d367d82b5984eebf959f38f8e70c6e022 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Mon, 19 Oct 2026 23:06:38 +0200 Subject: [PATCH 4/8] docs: S04 record and real-producer evidence Co-Authored-By: Claude Opus 5.5 --- .../S04-StableCanonicalSnapshot-PR.md | 138 +++++++++++++++++ documentation/evidence/s04-20261019/README.md | 15 ++ .../evidence/s04-20261019/real-producers.txt | 12 ++ .../session-1/benchmark-OpenStudio.json | 140 ++++++++++++++++++ .../s04-20261019/session-1/benchmark-TAS.json | 136 +++++++++++++++++ .../session-1/engine-comparison-run.json | 68 +++++++++ .../session-2/benchmark-OpenStudio.json | 140 ++++++++++++++++++ .../s04-20261019/session-2/benchmark-TAS.json | 136 +++++++++++++++++ .../session-2/engine-comparison-run.json | 68 +++++++++ .../tools/S04EvidenceHarness.cs.txt | 108 ++++++++++++++ 10 files changed, 961 insertions(+) create mode 100644 documentation/S04-StableCanonicalSnapshot-PR.md create mode 100644 documentation/evidence/s04-20261019/README.md create mode 100644 documentation/evidence/s04-20261019/real-producers.txt create mode 100644 documentation/evidence/s04-20261019/session-1/benchmark-OpenStudio.json create mode 100644 documentation/evidence/s04-20261019/session-1/benchmark-TAS.json create mode 100644 documentation/evidence/s04-20261019/session-1/engine-comparison-run.json create mode 100644 documentation/evidence/s04-20261019/session-2/benchmark-OpenStudio.json create mode 100644 documentation/evidence/s04-20261019/session-2/benchmark-TAS.json create mode 100644 documentation/evidence/s04-20261019/session-2/engine-comparison-run.json create mode 100644 documentation/evidence/s04-20261019/tools/S04EvidenceHarness.cs.txt diff --git a/documentation/S04-StableCanonicalSnapshot-PR.md b/documentation/S04-StableCanonicalSnapshot-PR.md new file mode 100644 index 00000000..1b9941cb --- /dev/null +++ b/documentation/S04-StableCanonicalSnapshot-PR.md @@ -0,0 +1,138 @@ +# 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 PRs add the same +`using System.Text.Json.Nodes;` line to `Modify/RunOpenStudioSimulation.cs` and otherwise touch different hunks; the +combination was merged and tested locally (see the S03/S04 integration note in the PR description). +Branch: `feature/s04-stable-canonical-snapshot`. Draft PR, not merged. **Validated content SHA: `0594e708`** (code and tests). +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). Awaiting hosted CI +and owner review. Not merged. + +## 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. +- **`Modify.WithViewState`:** the OpenStudio result model is built from the frozen copy. Before it is saved, it gets a copy + of the open model's view state back, so it opens with the views it had before S04. +- **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`: 13 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`): + **44/44 PASS**, 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`) on `0594e708`: **2873/2873 PASS** (2860 before plus 13 new). + +## 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. + +## 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 +Owner review of the draft PR. After merge, add the post-merge `PROJECT_PROGRESS.md` closeout on `sow/2026-Q4`. 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)"; + } + } +} From 420b71dd13b3053d250edcae41aad4cb4daf293b Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Mon, 19 Oct 2026 23:06:51 +0200 Subject: [PATCH 5/8] docs: S04 record whitespace Co-Authored-By: Claude Opus 5.5 --- documentation/S04-StableCanonicalSnapshot-PR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/S04-StableCanonicalSnapshot-PR.md b/documentation/S04-StableCanonicalSnapshot-PR.md index 1b9941cb..f7195ed2 100644 --- a/documentation/S04-StableCanonicalSnapshot-PR.md +++ b/documentation/S04-StableCanonicalSnapshot-PR.md @@ -36,7 +36,7 @@ and owner review. Not merged. - **`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. From 29ca1d1a53a994ef9453f4acbc34731960ca1399 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Mon, 19 Oct 2026 23:20:13 +0200 Subject: [PATCH 6/8] fix(ui): S04 review - view state copied at freeze time; empty UI set dropped - The OpenStudio result model's view state is now copied from the open model at the moment of the snapshot (on the run's thread, as the snapshot itself) and attached inside a try/catch: reading the live model at the end of a long run could throw and turn a successful run into Failed, and would have picked up edits made during the run. - An already-empty SAM.Analytical.UI set (written with no "Parameters") is left out of the snapshot like an emptied one; it carried only the per-build GUID. Co-Authored-By: Claude Opus 5.5 --- .../CanonicalSnapshotTests.cs | 22 ++++++++++- .../Modify/RunOpenStudioSimulation.cs | 27 +++++++++++-- .../Query/SimulationSnapshot.cs | 38 ++++++++++++++----- 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs b/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs index 8d12bc06..cd174ed0 100644 --- a/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs +++ b/WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs @@ -137,6 +137,22 @@ public void Snapshot_IsTheModelLessOnlyTheUiViewState() 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() @@ -230,7 +246,11 @@ public void OpenStudioResultModel_KeepsTheSourceViewState() AnalyticalModel analyticalModel_Frozen = Core.Convert.ToSAM(Freeze(analyticalModel, "frozen")).Single(); Assert.False(analyticalModel_Frozen.HasValue(AnalyticalModelParameter.UIGeometrySettings)); - AnalyticalModel analyticalModel_Result = Modify.WithViewState(analyticalModel_Frozen, analyticalModel); + //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()); diff --git a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs index efaaba43..7bf34624 100644 --- a/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs +++ b/WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs @@ -75,9 +75,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, analyticalModel, epwPath, progress, cancellationToken); + RunOpenStudioSimulation_Run(result, analyticalModel_Frozen, viewState, epwPath, progress, cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -168,7 +179,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, AnalyticalModel analyticalModel_Source, 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(); @@ -253,8 +264,16 @@ 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 it back. - WithViewState(analyticalModel_Result, analyticalModel_Source); + //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 index 094b54a3..e06a9810 100644 --- a/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs +++ b/WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs @@ -37,8 +37,14 @@ public static partial class Query 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; } @@ -62,21 +68,35 @@ public static partial class Query } } + 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 the view - /// state of , which left out of the snapshot. So a - /// result model opens with the views it opened with before S04. Returns . + /// 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, AnalyticalModel? analyticalModel_Source) + public static AnalyticalModel WithViewState(AnalyticalModel analyticalModel, Geometry.UI.UIGeometrySettings? viewState) { - if (analyticalModel != null - && analyticalModel_Source != null - && analyticalModel_Source.TryGetValue(UI.AnalyticalModelParameter.UIGeometrySettings, out Geometry.UI.UIGeometrySettings uIGeometrySettings) - && uIGeometrySettings != null) + if (analyticalModel != null && viewState != null) { - analyticalModel.SetValue(UI.AnalyticalModelParameter.UIGeometrySettings, new Geometry.UI.UIGeometrySettings(uIGeometrySettings)); + analyticalModel.SetValue(UI.AnalyticalModelParameter.UIGeometrySettings, new Geometry.UI.UIGeometrySettings(viewState)); } return analyticalModel!; From b19b80abbc128fbbc3f04dc182bcc8fe424e6bf7 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Mon, 19 Oct 2026 23:32:16 +0200 Subject: [PATCH 7/8] docs: S04 record - independent review fixes and final validation Co-Authored-By: Claude Opus 5.5 --- .../S04-StableCanonicalSnapshot-PR.md | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/documentation/S04-StableCanonicalSnapshot-PR.md b/documentation/S04-StableCanonicalSnapshot-PR.md index f7195ed2..11c8609e 100644 --- a/documentation/S04-StableCanonicalSnapshot-PR.md +++ b/documentation/S04-StableCanonicalSnapshot-PR.md @@ -3,10 +3,10 @@ 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 PRs add the same -`using System.Text.Json.Nodes;` line to `Modify/RunOpenStudioSimulation.cs` and otherwise touch different hunks; the -combination was merged and tested locally (see the S03/S04 integration note in the PR description). -Branch: `feature/s04-stable-canonical-snapshot`. Draft PR, not merged. **Validated content SHA: `0594e708`** (code and tests). +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 @@ -40,8 +40,11 @@ and owner review. Not merged. 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. -- **`Modify.WithViewState`:** the OpenStudio result model is built from the frozen copy. Before it is saved, it gets a copy - of the open model's view state back, so it opens with the views it had before S04. +- **`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`; @@ -63,7 +66,7 @@ and owner review. Not merged. input lost its UI-only state. ## Tests -New `CanonicalSnapshotTests.cs`: 13 tests in the WPF collection, on the Part O multizone fixture (2 flats plus a corridor, +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 @@ -96,7 +99,7 @@ Local results on this laptop. Dependencies: SAM `62b54156`, SAM_Tas `e84aeff6`, 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`): - **44/44 PASS**, including the real OpenStudio run. + **45/45 PASS** on `29ca1d1a`, including the real OpenStudio run. - **Mutation checks on `0594e708`** (each reverted), all caught: | Mutation | Tests failed | @@ -107,7 +110,25 @@ SAM_Validation `68eded0`, all on `sow/2026-Q4` and clean. | 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`) on `0594e708`: **2873/2873 PASS** (2860 before plus 13 new). +- 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 From dac1e5403175f4453f67a3c9108ec246af23e0ba Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Tue, 20 Oct 2026 06:54:51 +0200 Subject: [PATCH 8/8] docs: S04 record - integration with merged S03 and re-validation Co-Authored-By: Claude Opus 5.5 --- .../S04-StableCanonicalSnapshot-PR.md | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/documentation/S04-StableCanonicalSnapshot-PR.md b/documentation/S04-StableCanonicalSnapshot-PR.md index 11c8609e..b0c018ae 100644 --- a/documentation/S04-StableCanonicalSnapshot-PR.md +++ b/documentation/S04-StableCanonicalSnapshot-PR.md @@ -10,8 +10,9 @@ Branch: `feature/s04-stable-canonical-snapshot`. Draft PR, not merged. **Validat 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). Awaiting hosted CI -and owner review. Not merged. +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 @@ -149,6 +150,27 @@ benchmark-tas and benchmark-openstudio. 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. @@ -156,4 +178,5 @@ S04 they had four canonical hashes (`6d455480…`, `fcb0cd9b…`, `a3df983c…`, - The Both runs in the evidence needed `SAM_COMMIT` for the developer-built producers. See S03 (SAM_UI#242). ## Next step -Owner review of the draft PR. After merge, add the post-merge `PROJECT_PROGRESS.md` closeout on `sow/2026-Q4`. +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.