Skip to content
Draft
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
28 changes: 26 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,29 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw "Rebuild failed: $($it.Repo)" }
}

- name: Check S11 results read-back projects are built
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'

$repo = '${{ env.REPO_NAME }}'

# S11: the reference library and its CLI are members of SAM_OpenStudio.sln and were rebuilt with it above.
# Fail if either is missing from the solution or produced no output.
$listed = @(& dotnet sln (Join-Path $repo 'SAM_OpenStudio.sln') list)
if ($LASTEXITCODE -ne 0) { throw "dotnet sln list failed" }
foreach ($p in @('SAM.Analytical.OpenStudio.ResultsReadBack', 'SAM.Analytical.OpenStudio.ResultsReadBack.Cli')) {
if (-not ($listed | Where-Object { $_.Trim() -eq "benchmark\$p\$p.csproj" })) { throw "$p is not in SAM_OpenStudio.sln" }
}
foreach ($f in @(
'benchmark/SAM.Analytical.OpenStudio.ResultsReadBack/bin/x64/Release/net8.0/SAM.Analytical.OpenStudio.ResultsReadBack.dll',
'benchmark/SAM.Analytical.OpenStudio.ResultsReadBack.Cli/bin/x64/Release/net8.0/os-results-readback.dll'
)) {
$path = Join-Path $repo $f
if (-not (Test-Path $path)) { throw "Expected S11 build output missing: $path" }
Write-Host "S11 build output present: $path"
}

- name: Run benchmark producer tests (offline)
shell: pwsh
run: |
Expand All @@ -316,7 +339,7 @@ jobs:
--logger 'console;verbosity=normal'
if ($LASTEXITCODE -ne 0) { throw "Benchmark producer tests failed" }

- name: Run S09 OpenStudio native-input read-back tests (offline)
- name: Run S09 native-input and S11 results read-back tests (offline)
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Expand All @@ -325,14 +348,15 @@ jobs:

# S09: the writer's ventilation policy and the fresh-process OSM/IDF read-back on the S08 signed-off fixtures with
# the SAM#212 Budapest EPW (from the SAM sibling). OpenStudio SDK NuGet only: no CLI, no EnergyPlus run.
# S11: the results round-trip reference on synthetic SQL/ESO files keyed by the S09 exports (no EnergyPlus run).
$testProject = Join-Path $repo 'benchmark/SAM.Analytical.OpenStudio.InputReadBack.Tests/SAM.Analytical.OpenStudio.InputReadBack.Tests.csproj'
Write-Host "==> Testing: $testProject"
& dotnet test $testProject `
-c Release `
'/p:Platform=x64' `
'/p:SAMVersion=${{ steps.ver.outputs.samversion }}' `
--logger 'console;verbosity=normal'
if ($LASTEXITCODE -ne 0) { throw "S09 read-back tests failed" }
if ($LASTEXITCODE -ne 0) { throw "S09/S11 read-back tests failed" }

- name: Upload S09 failure diagnostics
# Only when a step failed: the bounded text evidence (exit code, stdout/stderr, process identities, file list) the
Expand Down
30 changes: 30 additions & 0 deletions SAM_OpenStudio.sln
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAM.Analytical.OpenStudio.I
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAM.Analytical.OpenStudio.InputReadBack.Tests", "benchmark\SAM.Analytical.OpenStudio.InputReadBack.Tests\SAM.Analytical.OpenStudio.InputReadBack.Tests.csproj", "{3F2CC081-C850-4FE7-BEF7-E7BFC5699603}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAM.Analytical.OpenStudio.ResultsReadBack", "benchmark\SAM.Analytical.OpenStudio.ResultsReadBack\SAM.Analytical.OpenStudio.ResultsReadBack.csproj", "{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SAM.Analytical.OpenStudio.ResultsReadBack.Cli", "benchmark\SAM.Analytical.OpenStudio.ResultsReadBack.Cli\SAM.Analytical.OpenStudio.ResultsReadBack.Cli.csproj", "{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -180,6 +184,30 @@ Global
{3F2CC081-C850-4FE7-BEF7-E7BFC5699603}.Release|x64.Build.0 = Release|x64
{3F2CC081-C850-4FE7-BEF7-E7BFC5699603}.Release|x86.ActiveCfg = Release|x64
{3F2CC081-C850-4FE7-BEF7-E7BFC5699603}.Release|x86.Build.0 = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Debug|Any CPU.ActiveCfg = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Debug|Any CPU.Build.0 = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Debug|x64.ActiveCfg = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Debug|x64.Build.0 = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Debug|x86.ActiveCfg = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Debug|x86.Build.0 = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Release|Any CPU.ActiveCfg = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Release|Any CPU.Build.0 = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Release|x64.ActiveCfg = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Release|x64.Build.0 = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Release|x86.ActiveCfg = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301}.Release|x86.Build.0 = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Debug|Any CPU.ActiveCfg = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Debug|Any CPU.Build.0 = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Debug|x64.ActiveCfg = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Debug|x64.Build.0 = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Debug|x86.ActiveCfg = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Debug|x86.Build.0 = Debug|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Release|Any CPU.ActiveCfg = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Release|Any CPU.Build.0 = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Release|x64.ActiveCfg = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Release|x64.Build.0 = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Release|x86.ActiveCfg = Release|x64
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302}.Release|x86.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -196,6 +224,8 @@ Global
{EBF33B94-81C3-4D70-BD25-F10C0E119B6F} = {09EED85C-BE3C-7566-DC0E-2E8E43466740}
{850D2D75-52C9-4322-B233-234F72981C39} = {09EED85C-BE3C-7566-DC0E-2E8E43466740}
{3F2CC081-C850-4FE7-BEF7-E7BFC5699603} = {09EED85C-BE3C-7566-DC0E-2E8E43466740}
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C301} = {09EED85C-BE3C-7566-DC0E-2E8E43466740}
{7A1F3C52-0B6E-4D8A-9C21-5E11A0B7C302} = {09EED85C-BE3C-7566-DC0E-2E8E43466740}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4E0308FF-C8D5-4700-951A-859CB9D4FD09}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ public void NullAndEmptyDiagnostics_LeaveValidEmptyArrays()

BenchmarkDocument document = model.ToBenchmark(emptyDiagnostics);
Assert.That(document.Provenance.Warnings, Is.Not.Null.And.Empty, "Empty arrays still serialize as valid empty arrays");
Assert.That(document.Provenance.Notes, Is.Not.Null.And.Empty);
// S11 F3: the only note on a successful document with no diagnostics is the Total-basis label.
Assert.That(document.Provenance.Notes, Is.EqualTo(new[] { Modify.TotalBasisNote }));
Assert.That(BenchmarkValidator.Validate(document).IsValid, Is.True, "An empty-diagnostic document stays schema-valid");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"SAM.Analytical.OpenStudio"
],
"warnings": [],
"notes": []
"notes": [
"Cooling metrics are EnergyPlus Ideal Loads Supply Air Total cooling (sensible \u002B latent); heating is Total heating."
]
},
"model": {
"consumptionHeating": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright (c) 2020-2026 Michal Dengusiak & Jakub Ziolkowski and contributors

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Nodes;
using NUnit.Framework;
using SAM.Analytical.Benchmark;

namespace SAM.Analytical.OpenStudio.Benchmark.Tests
{
/// <summary>
/// S11 producer rules: the all-zero peak rule, unavailable consumption without zone keys, the F3 Total-basis note, the
/// three-argument mapping of exactly the captured instances, the closure of the --sam-results file, and its CLI guard.
/// </summary>
[TestFixture]
public class S11ProducerTests
{
private static OpenStudioSimulationResultSet ResultSet(double coolingPeakKw, int coolingHour, double coolingKwh, bool coolingKeyed = true, bool heatingKeyed = true)
{
string key = BenchmarkFixture.IdealLoadsKey();
Dictionary<string, double> empty = new Dictionary<string, double>();
return new OpenStudioSimulationResultSet(
heatingKeyed ? new Dictionary<string, double> { { key, 10.5 } } : empty,
coolingKeyed ? new Dictionary<string, double> { { key, coolingKwh } } : empty,
new Dictionary<string, double> { { key, 3.25 } },
new Dictionary<string, double> { { key, coolingPeakKw } },
new Dictionary<string, int> { { key, 100 } },
new Dictionary<string, int> { { key, coolingHour } },
3.25, 100, coolingPeakKw, coolingHour,
new Dictionary<string, double> { { key, 2.0 } },
new Dictionary<string, double> { { key, 0.0 } },
empty, empty, empty, empty, empty, empty, empty,
null, null, null, 1.0, 0, 0, 0);
}

private static BenchmarkDocument Document(OpenStudioSimulationResultSet resultSet, RunState state = RunState.Success)
{
AnalyticalModel model = BenchmarkFixture.SingleSpaceModel();
OpenStudioBenchmarkContext context = BenchmarkFixture.Context(model, resultSet);
context.State = state;
return model.ToBenchmark(context);
}

[Test]
public void AllZeroPeak_LoadIsAMeasuredZero_HourIsUnavailable()
{
BenchmarkDocument document = Document(ResultSet(0, 0, 0));
BenchmarkConditionResult cooling = document.Spaces.Single().Cooling;

Assert.That(cooling.PeakLoad.Available, Is.True);
Assert.That(cooling.PeakLoad.Value, Is.EqualTo(0));
Assert.That(cooling.PeakHour.Available, Is.False, "an all-zero year ties at every hour: no peak hour");
Assert.That(document.Model.PeakCoolingLoad.Value, Is.EqualTo(0));
Assert.That(document.Model.PeakCoolingHour.Available, Is.False);
Assert.That(document.Model.ConsumptionCooling.Available, Is.True, "a keyed zero consumption is a measured 0 kWh");
Assert.That(document.Model.ConsumptionCooling.Value, Is.EqualTo(0));
Assert.That(document.Spaces.Single().Heating.PeakHour.Value, Is.EqualTo(100), "a non-zero peak keeps its hour");
Assert.That(BenchmarkValidator.Validate(document).IsValid, Is.True);
}

[Test]
public void NoZoneKey_ConsumptionIsUnavailable_NotZero()
{
BenchmarkDocument document = Document(ResultSet(2.4, 4602, 0, coolingKeyed: false, heatingKeyed: false));

Assert.That(document.Model.ConsumptionCooling.Available, Is.False);
Assert.That(document.Model.ConsumptionCooling.Value, Is.Null);
Assert.That(document.Model.ConsumptionHeating.Available, Is.False);
Assert.That(BenchmarkValidator.Validate(document).IsValid, Is.True);
}

[Test]
public void TotalBasisNote_OnSuccessfulDocumentsOnly_ExactlyOnce()
{
BenchmarkDocument success = Document(BenchmarkFixture.ResultSet());
Assert.That(success.Provenance.Notes.Count(x => x == Modify.TotalBasisNote), Is.EqualTo(1));
Assert.That(Modify.TotalBasisNote, Is.EqualTo("Cooling metrics are EnergyPlus Ideal Loads Supply Air Total cooling (sensible + latent); heating is Total heating."));

AnalyticalModel model = BenchmarkFixture.SingleSpaceModel();
OpenStudioBenchmarkContext again = BenchmarkFixture.Context(model, BenchmarkFixture.ResultSet());
again.Notes.Add(Modify.TotalBasisNote);
Assert.That(model.ToBenchmark(again).Provenance.Notes.Count(x => x == Modify.TotalBasisNote), Is.EqualTo(1), "a context already carrying the note keeps one");

BenchmarkDocument failure = Document(null, RunState.Failure);
Assert.That(failure.Provenance.Notes, Has.None.EqualTo(Modify.TotalBasisNote));
}

[Test]
public void ThreeArgumentToBenchmark_MapsExactlyTheGivenInstances()
{
AnalyticalModel model = BenchmarkFixture.SingleSpaceModel();
OpenStudioBenchmarkContext context = BenchmarkFixture.Context(model, BenchmarkFixture.ResultSet());
OpenStudioBenchmarkInputs inputs = model.BenchmarkInputs(context);

Assert.That(BenchmarkSerializer.Serialize(model.ToBenchmark(context, inputs)), Is.EqualTo(BenchmarkSerializer.Serialize(model.ToBenchmark(context))), "the two-argument form is the three-argument form on fresh inputs");

inputs.SpaceResults.First(x => x.TryGetValue(Analytical.SpaceSimulationResultParameter.LoadType, out string t) && t == "Cooling").SetValue(Analytical.SpaceSimulationResultParameter.Load, 1111.0);
inputs.ModelResult.SetValue(AnalyticalModelSimulationResultParameter.ConsumptionHeating, 77.0);
BenchmarkDocument document = model.ToBenchmark(context, inputs);
Assert.That(document.Spaces.Single().Cooling.PeakLoad.Value, Is.EqualTo(1111.0), "the document follows the instances it is given, not the context's result set");
Assert.That(document.Model.ConsumptionHeating.Value, Is.EqualTo(77.0));
}

[Test]
public void SamResultsFile_RoundTrips_AndReMapsToByteIdenticalModelAndSpaces()
{
AnalyticalModel model = BenchmarkFixture.SingleSpaceModel();
OpenStudioBenchmarkContext context = BenchmarkFixture.Context(model, BenchmarkFixture.ResultSet());
OpenStudioBenchmarkInputs inputs = model.BenchmarkInputs(context);
BenchmarkDocument produced = model.ToBenchmark(context, inputs);

string path = Path.Combine(TestContext.CurrentContext.WorkDirectory, "s11-sam-results-" + Guid.NewGuid().ToString("N").Substring(0, 6) + ".json");
StringWriter output = new StringWriter();
Program.WriteSamResults(inputs, path, output);
Assert.That(output.ToString(), Does.Contain(BenchmarkHash.ComputeSha256(File.ReadAllBytes(path))));
byte[] bytes = File.ReadAllBytes(path);
Assert.That(bytes.Take(3), Is.Not.EqualTo(new byte[] { 0xEF, 0xBB, 0xBF }), "UTF-8 without BOM");
Assert.That(System.Text.Encoding.UTF8.GetString(bytes), Does.Not.Contain("\r\n"));

OpenStudioBenchmarkInputs read = OpenStudioBenchmarkInputs.Read(path);
BenchmarkDocument remapped = model.ToBenchmark(context, read);
JsonObject a = JsonNode.Parse(BenchmarkSerializer.Serialize(produced)) as JsonObject;
JsonObject b = JsonNode.Parse(BenchmarkSerializer.Serialize(remapped)) as JsonObject;
Assert.That(b["model"].ToJsonString(), Is.EqualTo(a["model"].ToJsonString()));
Assert.That(b["spaces"].ToJsonString(), Is.EqualTo(a["spaces"].ToJsonString()));
Assert.That(read.HeatingConsumptionKeyed && read.CoolingConsumptionKeyed, Is.True);

read.SpaceResults[0].SetValue(Analytical.SpaceSimulationResultParameter.Load, 1.0);
JsonObject altered = JsonNode.Parse(BenchmarkSerializer.Serialize(model.ToBenchmark(context, read))) as JsonObject;
Assert.That(altered["spaces"].ToJsonString(), Is.Not.EqualTo(a["spaces"].ToJsonString()), "an altered object no longer closes");
}

[Test]
public void SamResultsFile_ForeignSchema_IsRejected()
{
Assert.Throws<System.Text.Json.JsonException>(() => OpenStudioBenchmarkInputs.FromJsonObject(new JsonObject { ["schema"] = "other/1" }));
}

[Test]
public void SamResultsSameAsOut_IsAUsageError_BeforeAnyRun()
{
string directory = Path.Combine(TestContext.CurrentContext.WorkDirectory, "s11-cli-" + Guid.NewGuid().ToString("N").Substring(0, 6));
Directory.CreateDirectory(directory);
string model = Path.Combine(directory, "model.json");
string weather = Path.Combine(directory, "weather.epw");
File.WriteAllText(model, "{}");
File.WriteAllText(weather, "x");
string output = Path.Combine(directory, "b1a.json");

StringWriter standardOutput = new StringWriter();
StringWriter standardError = new StringWriter();
int exit = Program.Run(new[] { "--model", model, "--weather", weather, "--out", output, "--sam-results", output }, standardOutput, standardError);

Assert.That(exit, Is.EqualTo((int)BenchmarkExitCode.InvalidUsage), standardError.ToString());
Assert.That(File.Exists(output), Is.False);
}
}
}
Loading
Loading