Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
376 changes: 376 additions & 0 deletions WPF/SAM.Analytical.UI.WPF.Tests/CanonicalSnapshotTests.cs

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions WPF/SAM.Analytical.UI.WPF.Tests/OpenStudioSimulationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ public void RealRun_SimulatesAFrozenCopy_AndSavesAResultModelWithOnlyOpenStudioR
}

AnalyticalModel analyticalModel = Core.Convert.ToSAM<AnalyticalModel>(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);
Expand All @@ -258,6 +264,13 @@ public void RealRun_SimulatesAFrozenCopy_AndSavesAResultModelWithOnlyOpenStudioR
Assert.All(results, x => Assert.Equal(AnalyticalModelSimulationResult.OpenStudioSource, x.Source));
Assert.Single(result.GetAnalyticalModelSimulationResults());

//S04: the snapshot carries no view state; the result model carries the open model's, as before.
Assert.False(Core.Convert.ToSAM<AnalyticalModel>(run.SnapshotPath).First().HasValue(SAM.Analytical.UI.AnalyticalModelParameter.UIGeometrySettings));
Assert.True(result.TryGetValue(SAM.Analytical.UI.AnalyticalModelParameter.UIGeometrySettings, out SAM.Geometry.UI.UIGeometrySettings uIGeometrySettings_Result));
//Compared after the same JSON round trip the saved file makes: UIGeometrySettings itself does not round-trip its default
//view's appearance dictionary, before S04 as after.
Assert.Equal(new SAM.Geometry.UI.UIGeometrySettings(JsonNode.Parse(uIGeometrySettings.ToJsonObject().ToJsonString()).AsObject()).ToJsonObject().ToJsonString(), uIGeometrySettings_Result.ToJsonObject().ToJsonString());

//S03: the provenance of a real run names what actually ran - the CLI by hash, EnergyPlus by its own eplusout.err.
JsonObject provenance = JsonNode.Parse(File.ReadAllText(run.ManifestPath))["provenance"].AsObject();
JsonObject engine = provenance["engine"].AsObject();
Expand Down
46 changes: 42 additions & 4 deletions WPF/SAM.Analytical.UI.WPF/Modify/RunOpenStudioSimulation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,20 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod

result.SnapshotSha256 = Query.Sha256(result.SnapshotPath);

//S04: the open model's view state, copied at the moment of the snapshot, for the result model; never a reason to fail.
Geometry.UI.UIGeometrySettings? viewState = null;
try
{
viewState = Query.ViewState(analyticalModel);
}
catch (Exception exception)
{
result.Diagnostics.Add("Warning: the view state could not be copied for the result model: " + exception.GetType().Name + ": " + exception.Message);
}

cancellationToken.ThrowIfCancellationRequested();

RunOpenStudioSimulation_Run(result, analyticalModel_Frozen, epwPath, progress, cancellationToken);
RunOpenStudioSimulation_Run(result, analyticalModel_Frozen, viewState, epwPath, progress, cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Expand All @@ -102,15 +113,31 @@ public static OpenStudioSimulationRun RunOpenStudioSimulation(this AnalyticalMod
/// <summary>
/// Writes <paramref name="analyticalModel"/> to <paramref name="path"/> and returns the model read back from that file: an
/// independent copy whose content is exactly the file's. Null when either step fails.
/// <para>
/// The file is <see cref="Query.SimulationSnapshotJsonObject"/> 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 <c>Core.Convert.ToFile</c> writes a JSON file, so a model with no view state gives the
/// same bytes as before S04.
/// </para>
/// </summary>
public static AnalyticalModel? FreezeAnalyticalModel(this AnalyticalModel analyticalModel, string path)
{
if (analyticalModel == null || string.IsNullOrWhiteSpace(path))
if (analyticalModel == null || string.IsNullOrWhiteSpace(path) || !System.IO.Directory.Exists(Path.GetDirectoryName(path)))
{
return null;
}

JsonObject? jsonObject = Query.SimulationSnapshotJsonObject(analyticalModel);
if (jsonObject == null)
{
return null;
}

if (!Core.Convert.ToFile(analyticalModel, path))
if (Core.Query.SAMFileType(path) == Core.SAMFileType.Json)
{
File.WriteAllText(path, new JsonArray(jsonObject).ToJsonString(Core.Convert.SerializerOptions(Core.Formatting.Indented)));
}
else if (!Core.Convert.ToFile(new AnalyticalModel(jsonObject), path))
{
return null;
}
Expand Down Expand Up @@ -154,7 +181,7 @@ public static AnalyticalModel AddOpenStudioResults(this AnalyticalModel analytic
// Its own method, never inlined: every OpenStudio type is reached here, so a missing SAM_OpenStudio or OpenStudio assembly fails
// this method's compilation inside the caller's try, and is recorded on the run like any other failure.
[MethodImpl(MethodImplOptions.NoInlining)]
private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStudioSimulationRun, AnalyticalModel analyticalModel_Frozen, string? epwPath, IProgress<Core.OpenStudio.OpenStudioSimulationProgress>? progress, CancellationToken cancellationToken)
private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStudioSimulationRun, AnalyticalModel analyticalModel_Frozen, Geometry.UI.UIGeometrySettings? viewState, string? epwPath, IProgress<Core.OpenStudio.OpenStudioSimulationProgress>? progress, CancellationToken cancellationToken)
{
openStudioSimulationRun.SamOpenStudioVersion = typeof(OpenStudioConversionResult).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? typeof(OpenStudioConversionResult).Assembly.GetName().Version?.ToString();

Expand Down Expand Up @@ -265,6 +292,17 @@ private static void RunOpenStudioSimulation_Run(OpenStudioSimulationRun openStud
openStudioSimulationRun.ResultDiagnostics.AddRange(diagnostics);
}

//S04: the snapshot left the open model's view state out; the result model, which a person opens, gets back the copy taken
//with the snapshot. A failure here is a warning, never a failed run.
try
{
WithViewState(analyticalModel_Result, viewState);
}
catch (Exception exception)
{
openStudioSimulationRun.Diagnostics.Add("Warning: the result model was saved without the view state: " + exception.GetType().Name + ": " + exception.Message);
}

string resultModelPath = Path.Combine(openStudioSimulationRun.Directory!, Query.OpenStudioResultModelFileName(analyticalModel_Frozen.Name));
if (!Core.Convert.ToFile(analyticalModel_Result, resultModelPath))
{
Expand Down
105 changes: 105 additions & 0 deletions WPF/SAM.Analytical.UI.WPF/Query/SimulationSnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors

using System.Linq;
using System.Text.Json.Nodes;

namespace SAM.Analytical.UI.WPF
{
public static partial class Query
{
/// <summary>
/// The JSON of <paramref name="analyticalModel"/> as a frozen simulation snapshot holds it (roadmap S04, finding F7 of SAM_UI#239):
/// the model's own JSON without <c>UI Geometry Settings</c>, the view state SAM_UI keeps on a model (its views, their visibility,
/// cameras and legends).
/// <para>
/// That parameter never reaches a simulation, and SAM_UI regenerates it - with new GUIDs - every time a model is opened, so a
/// snapshot that carried it gave the same engineering model a different snapshot hash and canonical model hash in every session.
/// It is the only parameter left out; the same parameter is already left out of <c>SimulationResultProvenance.Fingerprint</c> 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.
/// </para>
/// <para>
/// <b>The model given is never changed</b>: this edits the freshly built JSON tree, not the model. Null for no model.
/// </para>
/// </summary>
public static JsonObject? SimulationSnapshotJsonObject(AnalyticalModel analyticalModel)
{
JsonObject? result = analyticalModel?.ToJsonObject();
if (result?["ParameterSets"] is not JsonArray parameterSets)
{
return result;
}

string name_ParameterSet = Core.Query.Name(typeof(UI.AnalyticalModelParameter).Assembly);
string? name_Parameter = Core.Attributes.ParameterProperties.Get(UI.AnalyticalModelParameter.UIGeometrySettings)?.Name;

foreach (JsonObject parameterSet in parameterSets.OfType<JsonObject>().Where(x => Text(x["Name"]) == name_ParameterSet).ToList())
{
//An empty set is written without "Parameters" at all: it is dropped like an emptied one.
if (parameterSet["Parameters"] is not JsonArray parameters)
{
if (parameterSet["Parameters"] == null)
{
parameterSets.Remove(parameterSet);
}

continue;
}

foreach (JsonNode? parameter in parameters.Where(x => x is JsonObject jsonObject && Text(jsonObject["Name"]) == name_Parameter).ToList())
{
parameters.Remove(parameter);
}

if (parameters.Count == 0)
{
parameterSets.Remove(parameterSet);
}
}

return result;
}

private static string? Text(JsonNode? jsonNode)
{
return jsonNode is JsonValue jsonValue && jsonValue.TryGetValue(out string? value) ? value : null;
}
}

public static partial class Query
{
/// <summary>
/// A copy of <paramref name="analyticalModel"/>'s view state (<c>UI Geometry Settings</c>), taken now - at freeze time, so that later
/// edits to the open model cannot reach it - for <see cref="Modify.WithViewState"/>; null when it has none.
/// </summary>
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
{
/// <summary>
/// Gives <paramref name="analyticalModel"/> - a model derived from a snapshot, such as the OpenStudio result model - a copy of
/// <paramref name="viewState"/> (<see cref="Query.ViewState"/> of the open model when it was frozen), which
/// <see cref="Query.SimulationSnapshotJsonObject"/> left out of the snapshot. So a result model opens with the views it opened with
/// before S04. Returns <paramref name="analyticalModel"/>.
/// </summary>
public static AnalyticalModel WithViewState(AnalyticalModel analyticalModel, Geometry.UI.UIGeometrySettings? viewState)
{
if (analyticalModel != null && viewState != null)
{
analyticalModel.SetValue(UI.AnalyticalModelParameter.UIGeometrySettings, new Geometry.UI.UIGeometrySettings(viewState));
}

return analyticalModel!;
}
}
}
Loading
Loading