From 47cc5dd828f37f8b8b58e03bc817d73e29d8c7e5 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Sat, 24 Oct 2026 18:21:19 +0200 Subject: [PATCH 1/3] fix(analytical): keep fractional SpaceSimulationResult UnmetHours (F-S11-2) SpaceSimulationResultParameter.UnmetHours metadata IntegerParameterValue(0, 8760) -> DoubleParameterValue(0, 8760). The integer metadata rounded EnergyPlus fractional not-met time on SetValue (Convert.ToInt32: 14.5 -> 14, 14.75 -> 15, 0.25 -> 0) and accepted out-of-range fractions (-0.25 -> 0, 8760.25 -> 8760). Member, ordinal (34), key "Unmet Hours", range and JSON model unchanged. Integer values in older saved results and Tas whole-hour counts read back with the same numerical value. OccupiedUnmetHours, OccupiedHours, UnmetHourFirstIndex and AdjacencyClusterSimulationResultParameter.UnmetHours stay integer. New SpaceSimulationResultUnmetHoursTests: 12 tests, 8 fail on the previous metadata, all pass after. Generated by Michal Dengusiak and CodeClaude Co-Authored-By: Claude Opus 5.5 --- .../SpaceSimulationResultParameter.cs | 4 +- .../SpaceSimulationResultUnmetHoursTests.cs | 183 ++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 SAM/SAM.Tests/SpaceSimulationResultUnmetHoursTests.cs diff --git a/SAM/SAM.Analytical/Enums/Parameter/SpaceSimulationResultParameter.cs b/SAM/SAM.Analytical/Enums/Parameter/SpaceSimulationResultParameter.cs index 1b2ae3f3c..c1ab27cc2 100644 --- a/SAM/SAM.Analytical/Enums/Parameter/SpaceSimulationResultParameter.cs +++ b/SAM/SAM.Analytical/Enums/Parameter/SpaceSimulationResultParameter.cs @@ -45,7 +45,9 @@ public enum SpaceSimulationResultParameter [ParameterProperties("Max Dry Bulb Temperature Index", "Maximal Dry Bulb Temperature Index"), DoubleParameterValue(0, 8759)] MaxDryBulbTemperatureIndex, [ParameterProperties("Min Dry Bulb Temperature Index", "Minimal Dry Bulb Temperature Index"), DoubleParameterValue(0, 8759)] MinDryBulbTemperatureIndex, - [ParameterProperties("Unmet Hours", "Unmet Hours"), IntegerParameterValue(0, 8760)] UnmetHours, + //Double: EnergyPlus reports fractional unmet time (e.g. 14.5 h); Tas whole-hour counts and integer values in + //older saved results read back unchanged. + [ParameterProperties("Unmet Hours", "Unmet Hours"), DoubleParameterValue(0, 8760)] UnmetHours, [ParameterProperties("Unmet Hour First Index", "Unmet Hour First Index"), IntegerParameterValue(0, 8760)] UnmetHourFirstIndex, [ParameterProperties("Occupied Unmet Hours", "Occupied Unmet Hours"), IntegerParameterValue(0, 8760)] OccupiedUnmetHours, diff --git a/SAM/SAM.Tests/SpaceSimulationResultUnmetHoursTests.cs b/SAM/SAM.Tests/SpaceSimulationResultUnmetHoursTests.cs new file mode 100644 index 000000000..23a6e1b02 --- /dev/null +++ b/SAM/SAM.Tests/SpaceSimulationResultUnmetHoursTests.cs @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright (c) 2020–2026 Michal Dengusiak & Jakub Ziolkowski and contributors + +using SAM.Analytical; +using SAM.Core; +using SAM.Geometry.Spatial; +using System.Linq; +using System.Text.Json.Nodes; +using Xunit; + +namespace SAM.Tests +{ + /// + /// F-S11-2: holds fractional time. EnergyPlus reports + /// unmet time in hours with fractions (OpenStudio S11, e.g. 14.5 h); the former integer metadata rounded it on + /// SetValue. Tas writes whole-hour counts, which must keep their numerical meaning, and results saved before the + /// change (integer JSON, e.g. "Value": 9) must still read back. + /// + public class SpaceSimulationResultUnmetHoursTests + { + //One space result as SAM_Tas saved it before this change: S10 acc2 s08-fix-box fixture.json (cooling, trimmed + //to the counters). The counters are integer JSON numbers. + private const string LegacyTasResultJson = @"{ + ""_type"": ""SAM.Analytical.SpaceSimulationResult,SAM.Analytical"", + ""ParameterSets"": [ + { + ""_type"": ""SAM.Core.ParameterSet"", + ""Name"": ""SAM.Analytical"", + ""Guid"": ""76cd41af-8835-46fc-9225-a7fea365eae1"", + ""Parameters"": [ + { ""Name"": ""Load Type"", ""Value"": ""Cooling"" }, + { ""Name"": ""Load Index"", ""Value"": 3878 }, + { ""Name"": ""Unmet Hour First Index"", ""Value"": 3879 }, + { ""Name"": ""Unmet Hours"", ""Value"": 9 }, + { ""Name"": ""Occupied Unmet Hours"", ""Value"": 9 } + ] + } + ], + ""Name"": ""FixBox"", + ""Guid"": ""b3a4c82e-3249-4e92-a58f-c255430cbdb2"", + ""Source"": ""SAM.Analytical.Tas"", + ""Reference"": ""{25309C7B-8B43-47F8-AE63-3E235CFE2A7B}"" +}"; + + private static SpaceSimulationResult Result() + { + return new SpaceSimulationResult("Zone 1", "SAM.Analytical.OpenStudio", "zone-reference"); + } + + private static SpaceSimulationResult SaveAndReopen(SpaceSimulationResult spaceSimulationResult) + { + Space space = new Space("Zone 1", new Point3D(0, 0, 0)); + AdjacencyCluster adjacencyCluster = new AdjacencyCluster(); + adjacencyCluster.AddObject(space); + adjacencyCluster.AddObject(spaceSimulationResult); + adjacencyCluster.AddRelation(space, spaceSimulationResult); + + AnalyticalModel analyticalModel = new AnalyticalModel("Model", null, null, null, adjacencyCluster); + string json = analyticalModel.ToJsonObject().ToJsonString(); + AnalyticalModel reopened = new AnalyticalModel(JsonNode.Parse(json) as JsonObject); + + return reopened.AdjacencyCluster.GetObjects().Single(); + } + + [Theory] + [InlineData(0.25)] + [InlineData(14.5)] + [InlineData(14.75)] + public void FractionalUnmetHours_SurviveSetValueAndGetValue(double unmetHours) + { + SpaceSimulationResult spaceSimulationResult = Result(); + + Assert.True(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, unmetHours)); + Assert.True(spaceSimulationResult.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out double value)); + Assert.Equal(unmetHours, value); + Assert.Equal(unmetHours, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.UnmetHours)); + } + + [Theory] + [InlineData(0.25)] + [InlineData(14.5)] + [InlineData(14.75)] + public void FractionalUnmetHours_SurviveAWholeModelSaveAndReopen(double unmetHours) + { + SpaceSimulationResult spaceSimulationResult = Result(); + spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, unmetHours); + + SpaceSimulationResult reopened = SaveAndReopen(spaceSimulationResult); + + Assert.True(reopened.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out double value)); + Assert.Equal(unmetHours, value); + } + + [Fact] + public void WholeHourCounts_KeepTheirValue_AsSetByTas() + { + //SAM_Tas Query.UnmetHours passes an int count. + SpaceSimulationResult spaceSimulationResult = Result(); + Assert.True(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, 292)); + + SpaceSimulationResult reopened = SaveAndReopen(spaceSimulationResult); + + Assert.True(reopened.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out double value)); + Assert.Equal(292.0, value); + Assert.True(reopened.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out int count)); + Assert.Equal(292, count); + } + + [Fact] + public void LegacyIntegerJson_ReadsBackUnchanged() + { + SpaceSimulationResult spaceSimulationResult = new SpaceSimulationResult(JsonNode.Parse(LegacyTasResultJson) as JsonObject); + + Assert.True(spaceSimulationResult.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out double unmetHours)); + Assert.Equal(9.0, unmetHours); + Assert.Equal(9, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.UnmetHours)); + + //The other counters are untouched by this change. + Assert.Equal(9, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.OccupiedUnmetHours)); + Assert.Equal(3879, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.UnmetHourFirstIndex)); + Assert.Equal(3878, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.LoadIndex)); + } + + [Fact] + public void LegacyIntegerJson_ResavedAndReopened_KeepsTheValue() + { + SpaceSimulationResult legacy = new SpaceSimulationResult(JsonNode.Parse(LegacyTasResultJson) as JsonObject); + + SpaceSimulationResult reopened = SaveAndReopen(legacy); + + Assert.True(reopened.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out double unmetHours)); + Assert.Equal(9.0, unmetHours); + Assert.Equal(9, reopened.GetValue(SpaceSimulationResultParameter.OccupiedUnmetHours)); + } + + [Fact] + public void Range_KeepsZeroAndBothBoundaries_AndRejectsValuesOutside() + { + SpaceSimulationResult spaceSimulationResult = Result(); + + Assert.True(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, 0.0)); + Assert.Equal(0.0, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.UnmetHours)); + + Assert.True(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, 8760.0)); + Assert.Equal(8760.0, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.UnmetHours)); + + //A rejected value leaves the stored one in place. + Assert.False(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, -0.25)); + Assert.False(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, 8760.25)); + Assert.False(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, -1)); + Assert.False(spaceSimulationResult.SetValue(SpaceSimulationResultParameter.UnmetHours, 8761)); + Assert.Equal(8760.0, spaceSimulationResult.GetValue(SpaceSimulationResultParameter.UnmetHours)); + } + + [Fact] + public void Missing_IsUnavailable_NotZero() + { + SpaceSimulationResult missing = SaveAndReopen(Result()); + Assert.False(missing.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out double _)); + Assert.False(missing.HasValue(SpaceSimulationResultParameter.UnmetHours)); + + SpaceSimulationResult zero = Result(); + zero.SetValue(SpaceSimulationResultParameter.UnmetHours, 0.0); + zero = SaveAndReopen(zero); + Assert.True(zero.TryGetValue(SpaceSimulationResultParameter.UnmetHours, out double value)); + Assert.Equal(0.0, value); + } + + [Fact] + public void Metadata_KeepsMemberOrdinalAndKey_OnlyTheValueTypeIsDouble() + { + Assert.Equal(34, (int)SpaceSimulationResultParameter.UnmetHours); + Assert.Equal("Unmet Hours", Core.Attributes.ParameterProperties.Get(SpaceSimulationResultParameter.UnmetHours).Name); + Assert.Equal(ParameterType.Double, Core.Attributes.ParameterValue.GetParameterType(SpaceSimulationResultParameter.UnmetHours)); + + //The other counters stay integers. + Assert.Equal(ParameterType.Integer, Core.Attributes.ParameterValue.GetParameterType(SpaceSimulationResultParameter.UnmetHourFirstIndex)); + Assert.Equal(ParameterType.Integer, Core.Attributes.ParameterValue.GetParameterType(SpaceSimulationResultParameter.OccupiedUnmetHours)); + Assert.Equal(ParameterType.Integer, Core.Attributes.ParameterValue.GetParameterType(SpaceSimulationResultParameter.OccupiedHours)); + Assert.Equal(ParameterType.Integer, Core.Attributes.ParameterValue.GetParameterType(AdjacencyClusterSimulationResultParameter.UnmetHours)); + } + } +} From eadffe50991dc8e5474df2172cf0425d07ca291a Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Sat, 24 Oct 2026 18:43:23 +0200 Subject: [PATCH 2/3] docs: F-S11-2 PR record (SAM-BIM/SAM#219); tested content 47cc5dd8 Record only: change, compatibility analysis, findings (SAM_Revit Integer parameter risk, NaN behaviour), local validation and next step. Generated by Michal Dengusiak and CodeClaude Co-Authored-By: Claude Opus 5.5 --- documentation/S11-FractionalUnmetHours-PR.md | 159 +++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 documentation/S11-FractionalUnmetHours-PR.md diff --git a/documentation/S11-FractionalUnmetHours-PR.md b/documentation/S11-FractionalUnmetHours-PR.md new file mode 100644 index 000000000..8785f64ff --- /dev/null +++ b/documentation/S11-FractionalUnmetHours-PR.md @@ -0,0 +1,159 @@ +# F-S11-2: fractional `SpaceSimulationResult` unmet hours (SAM) + +Branch `fix/s11-fractional-unmet-hours` → base `sow/2026-Q4` (cut from `b7c2c36a`). Draft PR: SAM-BIM/SAM#219. +Record date: 2026-10-24. Tested content SHA: `47cc5dd8` (this record is added by a later record-only commit). + +## Current status + +Draft, **not merged, not Ready**. Waiting for final-head CI and an independent review. No engine run, no merge, no +SAM_OpenStudio#34 integration, no installer/release work is authorised by this PR. + +## Why + +F-S11-2 (recorded in Draft SAM-BIM/SAM_OpenStudio#34, `documentation/S11-OpenStudio-ResultsRoundTrip-PR.md`): EnergyPlus +reports "Setpoint Not Met Time" in hours with fractions (sub-hourly timesteps), and the OpenStudio importer passes the +annual sum to `SpaceSimulationResult.SetValue(UnmetHours, double)`. The parameter was declared +`IntegerParameterValue(0, 8760)`, so `SetValue` converted with `System.Convert.ToInt32`, which **rounds half to even** +(not truncation): 0.25 → 0, 14.5 → 14, 14.75 → 15, 15.5 → 16. It also accepted out-of-range fractions after rounding +(−0.25 → 0, 8760.25 → 8760). B1a already reads unmet hours as a double, so the loss happened inside SAM. + +F-S11-1 (unrelated identity fix) is already merged as SAM-BIM/SAM_OpenStudio#35 (`4b020585`, closeout `194ee1e6`); this PR +does not touch matching. + +## Change + +One line of metadata in `SAM/SAM.Analytical/Enums/Parameter/SpaceSimulationResultParameter.cs`: + +`[ParameterProperties("Unmet Hours", "Unmet Hours"), IntegerParameterValue(0, 8760)] UnmetHours` +→ `... DoubleParameterValue(0, 8760)] UnmetHours` (plus a two-line comment). + +Kept: enum member, ordinal 34, stored key `"Unmet Hours"` in the `SAM.Analytical` parameter set, range [0, 8760] +inclusive, the JSON model. Unchanged on purpose: `OccupiedUnmetHours`, `OccupiedHours`, `OccupiedHours25/28`, +`UnmetHourFirstIndex`, `LoadIndex` and `AdjacencyClusterSimulationResultParameter.UnmetHours` stay integer. No parallel +parameter, no schema version, no migration of saved files. + +## Why this is compatible (from the source, not assumed) + +- **Storage is untyped.** `ParameterSet` keeps `object` values; the attribute only decides the conversion on `SetValue` + (`ParameterizedSAMObject.SetValue` → `ParameterValue.TryConvert`). Reads (`TryGetValue`) convert the stored object + with `Core.Query.TryConvert`, whatever the attribute says. +- **JSON.** `ParameterSet.ReadParameterValue` reads a JSON number without `.`/`e` as `int`, otherwise `double`. Old files + (`"Value": 9`) therefore load as `Int32` and stay `Int32` in memory until re-set; `TryGetValue` gives 9.0 and + `TryGetValue` gives 9, exactly as before. New writes store a `double`; `Query.ToJsonNode` keeps the decimal + (`292.0`, `14.5`) so it reads back as a double. +- **Tas.** `SAM_Tas` `Query.UnmetHours` passes an `int` count; it is now stored as the same-valued double (`292` → + `292.0`), so the numerical meaning is identical. Readers: `SAM_Tas` benchmark `ToBenchmark` (`TryGetValue`), + S10 harness `ResultSubject.Number` (any numeric), `WorkflowCalculator`/`SAM_Tas_Grasshopper` `Core.Modify.Copy` + (object round trip through `SetValue`). +- **OpenStudio.** `Convert.ToSAM` `SimulationResults.AddLoadTypeResult` passes the double; B1a `ToBenchmark` and the S11 + read-back `SubjectDocument` read `TryGetValue`. +- **Integer readers** of the new double get `Convert.ToInt32` rounding, i.e. the old stored value, so nothing that + asked for `int` sees a different number than before. No SAM code reads `SpaceSimulationResultParameter.UnmetHours` as + `int` (`ToDesignExplorer` reads the *adjacency-cluster* parameter, unchanged). +- Previously rounded values cannot be recovered from saved SAM JSON; saved files are not rewritten. + +## Findings for the owner / reviewer + +1. **SAM_Revit export — risk, not verified, not changed (outside this PR's write scope).** `SAM_Revit` + `ActiveSetting` maps `UnmetHours` to Revit Space parameters `SAM_UnmetHoursCooling` / `SAM_UnmetHoursHeating`, and + `Core.Revit.Modify.SetValues` passes the stored object to `Modify.SetValue(Parameter, object)`. `SetValue_Integer` + accepts `int`, `string`, `bool`, `IntegerId`, `LongId`, **not `double`**; `SetValue_Double` accepts any numeric. The + Revit parameter's storage type comes from the shared-parameter workbook used by `GenerateSharedParametersFile`, which is + not in any SAM-BIM repository, so it could not be checked. If those parameters are **Integer**, results set after this + change (now `double`, including Tas whole hours) would silently stop being written to Revit; if they are **Number**, + nothing changes. Old models loaded from JSON still hold `Int32` and are unaffected until re-simulated. + Proposed dependency fix (separate SAM_Revit PR, needs owner approval before editing): in + `SAM.Core.Revit/Modify/SetValue.cs` `SetValue_Integer`, add a branch for a finite whole-number `double` within `int` + range → `parameter.Set((int)value)`; leave fractional doubles refused (no silent rounding into an Integer parameter). + Or confirm the two parameters are Number type, in which case nothing is needed. +2. **NaN now stored instead of throwing.** Before: `SetValue(UnmetHours, double.NaN)` threw `OverflowException` (from + `Convert.ToInt32`). After: it is stored, as for every other ranged `DoubleParameterValue` (`DoubleParameterValue` + range checks are false for NaN). ±∞ are refused by the range. Producers do not set NaN (OpenStudio sums SQL rows, + Tas counts), B1a readers drop non-finite values (`IsFinite`), but a NaN would make a later `TryGetValue` throw. + Not changed here (shared `SAM.Core` attribute behaviour); listed for the owner. +3. Small, intended behaviour differences: a numeric string `"14.5"` is now accepted (was refused by `int.TryParse`); + `SAM_UI` `CustomParameter.ParameterTypes` now offers `double` for this parameter, so the property grid shows and edits + fractions. + +## Tests + +`SAM/SAM.Tests/SpaceSimulationResultUnmetHoursTests.cs` (xUnit, 12 tests): 0.25 / 14.5 / 14.75 through `SetValue` / +`GetValue` and through a whole `AnalyticalModel` JSON save/reopen; a Tas `int` count (292) keeps its value; an old Tas +result (integer JSON, shape taken from S10 acc2 `s08-fix-box/fixture.json`) reads back 9 and survives re-save; zero and +both boundaries kept, −0.25 / 8760.25 / −1 / 8761 refused without changing the stored value; missing ≠ zero; member +ordinal 34, key and value types (this one Double, the other counters Integer). + +## Validation (local, 2026-10-24, Windows, .NET SDK 10.0.401) + +All builds used isolated outputs and `-p:APPDATA=`, so no Grasshopper/`%APPDATA%\SAM` deployment happened. +Base SAM `b7c2c36a` → `SAM.Analytical.dll` `017370d9…`; tested SAM `47cc5dd8` → `SAM.Analytical.dll` `eacd1ed8…` +(full hashes in the local evidence `dll-hashes.sha256`). Every consumer test binary below was checked to load `eacd1ed8…`. + +| Check | Result | +|---|---| +| New tests on the **base** metadata (`b7c2c36a` + new test file) | **8 failed, 4 passed** of 12: 0.25 → 0, 14.5 → 14, 14.75 → 15 (both `SetValue` and save/reopen), −0.25 accepted, type Integer | +| New tests at `47cc5dd8` | 12/12 pass | +| Full `SAM.Tests` at `47cc5dd8` | **3738/3738** pass (3726 existing + 12 new), 0 skipped | +| OpenStudio SQL → SAM probe, base SAM | heating 14.5 h → **14**, cooling 14.75 h → **15** (fails, reproduces F-S11-2) | +| OpenStudio SQL → SAM probe, tested SAM | **14.5 / 14.75** mapped by `Modify.AddResults(adjacencyCluster, sql)` and identical after `AnalyticalModel` JSON save/reopen; pass | +| SAM_OpenStudio Q4 `194ee1e6` (includes #35), unmodified, `tests/SAM.Analytical.OpenStudio.Tests`, `TestCategory!=Simulation` | 488 passed, 2 skipped (real-SQL fixtures not present locally), 0 failed | +| SAM_OpenStudio Q4 B1a `benchmark/SAM.Analytical.OpenStudio.Benchmark.Tests`, `TestCategory!=Simulation` | 51/51 | +| SAM_Tas Q4 `922044fa` `benchmark/SAM.Analytical.Tas.Benchmark.Tests` | 40/40 | +| SAM_Tas Q4 `SAM_Tas/SAM.Analytical.Tas.DirectT3D.ReadBack.Tests` (incl. S10 synthetic triple, S08/S10 offline replays, runner-script doubles) | 249/249 | +| Real S10 acc2 saved models (`C:\TasOut\s10\acc2\s08-fix-box\fixture.json` and `...\s08-fix-two\fixture.json`, read-only) loaded with base and tested SAM | **identical**: FixBox cooling 9 / heating 292, FixA 0 / 40, FixB 0 / 0 h, still `Int32`-backed; `OccupiedUnmetHours`, `UnmetHourFirstIndex` unchanged; equal to the accepted acc2 `subject.json` and `b1a.json` values; both fixture hashes unchanged | + +**Not run / limits.** +- S10 `results-subject` regeneration and `Run-S10-Results.ps1`: not run. `results-subject` reopens the TSD through Tas COM + (`Convert.ToSAM_AnalyticalModelSimulationResult`), a Tas operation this task does not authorise. The `results-compare` + replay reads an existing `subject.json` (already numbers) and does not exercise this metadata, so it would prove + nothing here. The accepted S10 baseline (including its one BLOCKED FixB heating design-day-hour row) is untouched. +- No EnergyPlus, OpenStudio CLI or Tas run. **Disclosure:** a first SAM_OpenStudio test run in a deep scratch folder was + started without CI's `TestCategory!=Simulation` filter and stopped. Its `Simulation`-category tests all failed at + "The OSM could not be saved" (path too long) before any CLI call; no `eplusout*`, `*.err`, `in.idf` or `run.log` was + produced and no EnergyPlus/OpenStudio process was running. Its log is kept as evidence; the reported results above + come from the rerun with the CI filter in a short path. +- SAM_Revit cannot be built or exercised here (Revit API); finding 1 is from source reading only. +- Hosted CI: see the PR description for the final head. + +**Provenance of probe dependencies (machine-local copies).** SAM_SQLite `build/` from the local checkout at `ec2eb86` +(`SAM.Core.SQLite.dll` `ba9400e9…`); SAM_Validation `build/` from the local checkout at Q4 `2a3acfaf` +(`SAM.Analytical.Benchmark.dll` `0dfdc8ec…`). SAM_OpenStudio and SAM_Tas sources were `git archive` exports of their +Q4 heads; the only edits were in the probe copy (below), never in a repository. + +**Commands** (from the worktree, `$A` = a scratch APPDATA folder): + +``` +dotnet test SAM/SAM.Tests/SAM.Tests.csproj -c Release -p:APPDATA=$A --filter "FullyQualifiedName~SpaceSimulationResultUnmetHoursTests" +dotnet test SAM/SAM.Tests/SAM.Tests.csproj -c Release -p:APPDATA=$A +``` + +Consumer suites: export SAM_OpenStudio and SAM_Tas Q4 next to a `SAM/build` holding the tested DLLs (plus +`SAM_SQLite/build`, `SAM_Validation/build`), keep the path short (MAX_PATH), then +`dotnet test -c Release -p:Platform=x64 -p:APPDATA=$A --filter "TestCategory!=Simulation"` (OpenStudio) or +`dotnet test -c Release -p:APPDATA=$A` (Tas). OpenStudio probe: `FS112FractionalUnmetProbe.cs` added to the +exported `tests/SAM.Analytical.OpenStudio.Tests/C5/`, with `AddResultsBySqlTests.CreateResultsSql`/name helpers made +`internal` and the not-met rows changed to `0.625` per hour with `0.125` (heating) / `0.375` (cooling) at the peak hour +(23 × 0.625 + 0.125 = 14.5, 23 × 0.625 + 0.375 = 14.75; binary-exact). + +**Evidence (machine-local, not committed; copy before switching machines):** +`SAM-BIM/.f-s11-2-20261024/` — `sam-tests/` (before/after/full TRX), `os-probe/` (probe source, patched test copy, +before/after logs, the stopped unfiltered run log), `logs/` (consumer suite logs/TRX), `fixture-probe/` (source, base/after +outputs, fixture hashes), `behaviour-probe/` (NaN and edge cases, base/after), `dll-hashes.sha256`, `ws/` (exported +workspaces). + +## Files changed + +- `SAM/SAM.Analytical/Enums/Parameter/SpaceSimulationResultParameter.cs` (metadata line + comment) +- `SAM/SAM.Tests/SpaceSimulationResultUnmetHoursTests.cs` (new) +- `documentation/S11-FractionalUnmetHours-PR.md` (this record) + +## Next step + +1. Independent review of this Draft PR at its final head (recommended: another model or a fresh Claude Opus 5.5 + session, High effort, new session, Plan mode, read-only — a fresh reader checks the compatibility argument without + this session's assumptions). +2. Owner decisions: finding 1 (confirm the Revit parameter type, or approve the small SAM_Revit `SetValue_Integer` + change as its own PR) and finding 2 (accept NaN behaviour or ask for a guard). +3. After owner approval and merge: known-SHA `PROJECT_PROGRESS.md` closeout on SAM `sow/2026-Q4`; then, as a separate + reviewed step, integrate into SAM_OpenStudio#34 and reconcile its pinned F-S11-2 expected-failure test (left untouched + here). From db48598b30cf2711af40a96cb55bbe6c5e5fb713 Mon Sep 17 00:00:00 2001 From: Michal Dengusiak Date: Sat, 24 Oct 2026 20:15:15 +0200 Subject: [PATCH 3/3] docs: F-S11-2 record - owner defers Revit compatibility; eadffe50 checks green Record only. Owner deferral of SAM_Revit compatibility (finding 1), independent review acceptance of the core fix, exact-head hosted checks at eadffe50 (Build, Test 3738/3738, SPDX), next step = owner merge approval. Tested content unchanged: 47cc5dd8. Generated by Michal Dengusiak and CodeClaude Co-Authored-By: Claude Opus 5.5 --- documentation/S11-FractionalUnmetHours-PR.md | 39 +++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/documentation/S11-FractionalUnmetHours-PR.md b/documentation/S11-FractionalUnmetHours-PR.md index 8785f64ff..8ec14d9d7 100644 --- a/documentation/S11-FractionalUnmetHours-PR.md +++ b/documentation/S11-FractionalUnmetHours-PR.md @@ -1,12 +1,14 @@ # F-S11-2: fractional `SpaceSimulationResult` unmet hours (SAM) Branch `fix/s11-fractional-unmet-hours` → base `sow/2026-Q4` (cut from `b7c2c36a`). Draft PR: SAM-BIM/SAM#219. -Record date: 2026-10-24. Tested content SHA: `47cc5dd8` (this record is added by a later record-only commit). +Record date: 2026-10-24. Tested content SHA: `47cc5dd8`. Every later commit is record-only: `eadffe50` added this +record; the owner-deferral update that follows changes only this file. The PR description names the final head. ## Current status -Draft, **not merged, not Ready**. Waiting for final-head CI and an independent review. No engine run, no merge, no -SAM_OpenStudio#34 integration, no installer/release work is authorised by this PR. +Draft, **not merged, not Ready**. An independent review **accepted the core fix** at `eadffe50`, and verified Build, Test +and SPDX passing there. The owner **deferred Revit compatibility** (finding 1, below). Waiting for owner merge approval. +No engine run, no merge, no SAM_OpenStudio#34 integration, no installer/release work is authorised by this PR. ## Why @@ -54,7 +56,10 @@ parameter, no schema version, no migration of saved files. ## Findings for the owner / reviewer -1. **SAM_Revit export — risk, not verified, not changed (outside this PR's write scope).** `SAM_Revit` +1. **SAM_Revit export — DEFERRED by the owner (2026-10-24).** Owner decision, given in chat: Revit is unavailable and + its shared-parameter definitions can be changed later, so no Revit fix is made now; SAM keeps fractional unmet + hours. The risk below stays open as a known, accepted follow-up, not a blocker for this PR. Original analysis: + risk, not verified, not changed (outside this PR's write scope). `SAM_Revit` `ActiveSetting` maps `UnmetHours` to Revit Space parameters `SAM_UnmetHoursCooling` / `SAM_UnmetHoursHeating`, and `Core.Revit.Modify.SetValues` passes the stored object to `Modify.SetValue(Parameter, object)`. `SetValue_Integer` accepts `int`, `string`, `bool`, `IntegerId`, `LongId`, **not `double`**; `SetValue_Double` accepts any numeric. The @@ -62,7 +67,7 @@ parameter, no schema version, no migration of saved files. not in any SAM-BIM repository, so it could not be checked. If those parameters are **Integer**, results set after this change (now `double`, including Tas whole hours) would silently stop being written to Revit; if they are **Number**, nothing changes. Old models loaded from JSON still hold `Int32` and are unaffected until re-simulated. - Proposed dependency fix (separate SAM_Revit PR, needs owner approval before editing): in + Possible later fix, **not authorised now** (a separate SAM_Revit PR, or a change to the shared-parameter definitions): in `SAM.Core.Revit/Modify/SetValue.cs` `SetValue_Integer`, add a branch for a finite whole-number `double` within `int` range → `parameter.Set((int)value)`; leave fractional doubles refused (no silent rounding into an Integer parameter). Or confirm the two parameters are Number type, in which case nothing is needed. @@ -113,7 +118,13 @@ Base SAM `b7c2c36a` → `SAM.Analytical.dll` `017370d9…`; tested SAM `47cc5dd8 produced and no EnergyPlus/OpenStudio process was running. Its log is kept as evidence; the reported results above come from the rerun with the CI filter in a short path. - SAM_Revit cannot be built or exercised here (Revit API); finding 1 is from source reading only. -- Hosted CI: see the PR description for the final head. +- Hosted CI: below; the PR description names the final head and its checks. + +**Hosted checks at `eadffe50`** (record-only on top of tested `47cc5dd8`), all SUCCESS, also verified by the +independent review: Build (Windows) [40112883313](https://github.com/SAM-BIM/SAM/actions/runs/40112883313), Test +[40112883328](https://github.com/SAM-BIM/SAM/actions/runs/40112883328) (synthetic merge `dfeadc3` of `eadffe50` into +`b7c2c36a`; 3738/3738 passed, including the 12 new tests), SPDX +[40112883247](https://github.com/SAM-BIM/SAM/actions/runs/40112883247). **Provenance of probe dependencies (machine-local copies).** SAM_SQLite `build/` from the local checkout at `ec2eb86` (`SAM.Core.SQLite.dll` `ba9400e9…`); SAM_Validation `build/` from the local checkout at Q4 `2a3acfaf` @@ -149,11 +160,11 @@ workspaces). ## Next step -1. Independent review of this Draft PR at its final head (recommended: another model or a fresh Claude Opus 5.5 - session, High effort, new session, Plan mode, read-only — a fresh reader checks the compatibility argument without - this session's assumptions). -2. Owner decisions: finding 1 (confirm the Revit parameter type, or approve the small SAM_Revit `SetValue_Integer` - change as its own PR) and finding 2 (accept NaN behaviour or ask for a guard). -3. After owner approval and merge: known-SHA `PROJECT_PROGRESS.md` closeout on SAM `sow/2026-Q4`; then, as a separate - reviewed step, integrate into SAM_OpenStudio#34 and reconcile its pinned F-S11-2 expected-failure test (left untouched - here). +1. Owner merge approval at the final head named in the PR description, after its checks pass. Finding 1 is deferred; + finding 2 (NaN) is unchanged and stays a recorded note. Recommended executor once approved: Claude Code, Claude + Sonnet 5.5, Medium effort, existing session, Build mode. It is a mechanical guarded merge plus closeout, with the + context already here. +2. After merge: known-SHA `PROJECT_PROGRESS.md` closeout on SAM `sow/2026-Q4` (docs-only, `[skip ci]`), carrying the Revit + deferral; guarded cleanup of `fix/s11-fractional-unmet-hours`. +3. Then, as a separate reviewed step, integrate into SAM_OpenStudio#34 and reconcile its pinned F-S11-2 expected-failure + test (left untouched here). The Revit follow-up waits for its own owner decision.