From 752d315ab3c13f9f2a9bee607e7e6c7897064748 Mon Sep 17 00:00:00 2001 From: heslinge Date: Mon, 27 Jul 2026 10:58:28 -0600 Subject: [PATCH] Add windows measure --- lib/BOSS/boss.rb | 2 + .../buildingsync_reader.rb | 47 +++++ lib/BOSS/buildingsync_reader/systems_map.rb | 38 ++++ lib/BOSS/osw_arg_populator.rb | 22 +++ spec/tests/unit/buildingsync_reader_spec.rb | 166 ++++++++++++++++-- 5 files changed, 257 insertions(+), 18 deletions(-) diff --git a/lib/BOSS/boss.rb b/lib/BOSS/boss.rb index e562777..f998e7f 100644 --- a/lib/BOSS/boss.rb +++ b/lib/BOSS/boss.rb @@ -48,6 +48,8 @@ def write_baseline_osw(xml_file_path, output_dir, epw_file_path, standard_to_be_ OSWArgPopulator::populate_set_lighting_loads_by_LPD_args(baseline_osw, bsync_reader) OSWArgPopulator::populate_set_electric_equipment_loads_by_epd_args(baseline_osw, bsync_reader) + OSWArgPopulator::populate_replace_baseline_windows_args(baseline_osw, bsync_reader) + OSWArgPopulator::populate_openstudio_results_args(baseline_osw, bsync_reader) # Gather extension-derived paths (typically gem-based measures/files) from ObjectSpace. diff --git a/lib/BOSS/buildingsync_reader/buildingsync_reader.rb b/lib/BOSS/buildingsync_reader/buildingsync_reader.rb index 028a0b6..d625bd4 100644 --- a/lib/BOSS/buildingsync_reader/buildingsync_reader.rb +++ b/lib/BOSS/buildingsync_reader/buildingsync_reader.rb @@ -310,5 +310,52 @@ def get_total_weighted_average_load return nil if !all_weighted_average_loads.all? return all_weighted_average_loads.map {|s| s.to_f}.sum end + + # first valid window that has all mappable fields + def get_window_data + # if no windows, return nil + fenestration_systems = @facility_xml.elements.each("#{@ns}Systems/#{@ns}FenestrationSystems/#{@ns}FenestrationSystem") {|s| s} + return nil if fenestration_systems.nil? + + # get all windows + windows = fenestration_systems.select {|fs| fs.elements["#{@ns}FenestrationType/#{@ns}Window"] } + return nil if windows.empty? + + # iter through windows until we get one with all the required fields + windows.each do |window| + # get data + fenestration_frame_material = window.elements["#{@ns}FenestrationFrameMaterial"]&.text + glass_type = window.elements["#{@ns}GlassType"]&.text + fenestration_glass_layers = window.elements["#{@ns}FenestrationGlassLayers"]&.text + solar_heat_gain_coefficient = window.elements["#{@ns}SolarHeatGainCoefficient"]&.text + visible_transmittance = window.elements["#{@ns}VisibleTransmittance"]&.text + + # We only need one of fenestration_u_factor/fenestration_r_value + fenestration_u_factor = window.elements["#{@ns}FenestrationUFactor"]&.text + if fenestration_u_factor.nil? + fenestration_r_value = window.elements["#{@ns}FenestrationRValue"]&.text + if !fenestration_r_value.nil? then fenestration_u_factor = 1 / fenestration_r_value end + end + + # map data + os_fenestration_frame_material = BuildingSyncToOSSystemMaps.get_frame_material_map[fenestration_frame_material.to_s] + os_glass_type = BuildingSyncToOSSystemMaps.get_glass_type_map[glass_type.to_s] + os_fenestration_glass_layers = BuildingSyncToOSSystemMaps.get_glass_layers_map[fenestration_glass_layers.to_s] + + # return if we can use this one + if ( + !os_fenestration_frame_material.nil? && + !os_glass_type.nil? && + !os_fenestration_glass_layers.nil? && + !fenestration_u_factor.nil? && + !solar_heat_gain_coefficient.nil? && + !visible_transmittance.nil? + ) + window_pane_type = [os_fenestration_glass_layers, os_glass_type, os_fenestration_frame_material].join(' - ') + return window_pane_type, fenestration_u_factor, solar_heat_gain_coefficient, visible_transmittance + end + end + return nil + end end end diff --git a/lib/BOSS/buildingsync_reader/systems_map.rb b/lib/BOSS/buildingsync_reader/systems_map.rb index 32989a5..8ac2fde 100644 --- a/lib/BOSS/buildingsync_reader/systems_map.rb +++ b/lib/BOSS/buildingsync_reader/systems_map.rb @@ -26,4 +26,42 @@ def self.get_hvac_map "Other" => "" } end + + def self.get_glass_layers_map + return { + "Single pane" => "Single", + "Double pane" => "Double", + "Triple pane" => "Triple", + "Single paned with storm panel" => "Single", + } + end + + def self.get_glass_type_map + return { + "Clear uncoated" => "No LowE - Clear", + "Low e" => "LowE - Clear", + "Tinted" => "No LowE - Tinted/Reflective", + "Tinted plus low e" => "LowE - Tinted/Reflective", + "Reflective" => "No LowE - Tinted/Reflective", + "Reflective on tint" => "No LowE - Tinted/Reflective", + "High performance tint" => "LowE - Tinted/Reflective", + "Sunbelt low E low SHGC" => "LowE - Tinted/Reflective", + # "Suspended film" => "", + # "Plastic" => "", + } + end + + def self.get_frame_material_map + return { + "Aluminum uncategorized" => "Aluminum", + "Aluminum no thermal break" => "Aluminum", + "Aluminum thermal break" => "Thermally Broken Aluminum", + # "Clad" => "", + # "Composite" => "", + # "Fiberglass" => "", + # "Steel" => "", + # "Vinyl" => "", + "Wood" => "Wood", + } + end end diff --git a/lib/BOSS/osw_arg_populator.rb b/lib/BOSS/osw_arg_populator.rb index 2d899d0..070eea5 100644 --- a/lib/BOSS/osw_arg_populator.rb +++ b/lib/BOSS/osw_arg_populator.rb @@ -183,6 +183,28 @@ def self.populate_set_electric_equipment_loads_by_epd_args(osw, bsync_reader) # om_frequency end + def self.populate_replace_baseline_windows_args(osw, bsync_reader) + osw[:steps].append({"measure_dir_name": "replace_baseline_windows", "arguments": {}}) + set_measure_argument = lambda {| key, value | OpenStudio::Extension.set_measure_argument(osw, "replace_baseline_windows", key, value) } + + # skip if no window_data + window_data = bsync_reader.get_window_data + if window_data.nil? + set_measure_argument.call("__SKIP__", true) + return + end + window_pane_type, fenestration_u_factor, solar_heat_gain_coefficient, visible_transmittance = window_data + + # Add args + # - __SKIP__ + set_measure_argument.call("__SKIP__", false) + set_measure_argument.call("window_pane_type", window_pane_type) + set_measure_argument.call("u_value_ip", fenestration_u_factor) + set_measure_argument.call("shgc", solar_heat_gain_coefficient) + set_measure_argument.call("vlt", visible_transmittance) + + end + def self.populate_openstudio_results_args(osw, bsync_reader) osw[:steps].append({"measure_dir_name": "openstudio_results", "arguments": {}}) set_measure_argument = lambda {| key, value | OpenStudio::Extension.set_measure_argument(osw, "openstudio_results", key, value) } diff --git a/spec/tests/unit/buildingsync_reader_spec.rb b/spec/tests/unit/buildingsync_reader_spec.rb index d7317d0..91d9d2a 100644 --- a/spec/tests/unit/buildingsync_reader_spec.rb +++ b/spec/tests/unit/buildingsync_reader_spec.rb @@ -23,6 +23,24 @@ def wrap_in_site(xml) XML end +def wrap_in_systems(xml) + <<~XML + + + + + + + + + #{xml} + + + + + XML +end + RSpec.describe 'BuildingSyncReader' do describe 'get_climate_zone should' do it "get from site" do @@ -334,24 +352,6 @@ def wrap_in_systems(xml) end describe 'get_total_weighted_average_load should' do - def wrap_in_systems(xml) - <<~XML - - - - - - - - - #{xml} - - - - - XML - end - it "sum weighted average load" do # Set Up doc = REXML::Document.new wrap_in_systems(<<~XML) @@ -423,4 +423,134 @@ def wrap_in_systems(xml) expect(buidingsync_reader.get_total_weighted_average_load).to eq nil end end + + describe 'get_window_data should' do + good_window = <<~XML + + + + + Aluminum no thermal break + false + Average + Clear uncoated + Single pane + 0.391000 + 0.391000 + 3.241000 + + XML + invalid_window = <<~XML + + + + + Fiberglass + false + Average + Clear uncoated + Single pane + 0.391000 + 0.391000 + 3.241000 + + XML + door = <<~XML + + + + Uninsulated metal + + + + XML + + it "work in happy case" do + # Set Up + doc = REXML::Document.new wrap_in_systems(<<~XML) + + #{good_window} + + XML + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + window_pane_type, fenestration_u_factor, solar_heat_gain_coefficient, visible_transmittance = buidingsync_reader.get_window_data + + # Assertion + expect(window_pane_type).to eq "Single - No LowE - Clear - Aluminum" + expect(fenestration_u_factor).to eq "3.241000" + expect(solar_heat_gain_coefficient).to eq "0.391000" + expect(visible_transmittance).to eq "0.391000" + end + + it "ignore doors and skylights" do + # Set Up + doc = REXML::Document.new wrap_in_systems(<<~XML) + + #{door} + #{good_window} + + XML + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + window_pane_type, fenestration_u_factor, solar_heat_gain_coefficient, visible_transmittance = buidingsync_reader.get_window_data + + # Assertion + expect(window_pane_type).to eq "Single - No LowE - Clear - Aluminum" + expect(fenestration_u_factor).to eq "3.241000" + expect(solar_heat_gain_coefficient).to eq "0.391000" + expect(visible_transmittance).to eq "0.391000" + end + + it "work if first window is invalid but second isnt" do + # Set Up + doc = REXML::Document.new wrap_in_systems(<<~XML) + + #{invalid_window} + #{good_window} + + XML + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + window_pane_type, fenestration_u_factor, solar_heat_gain_coefficient, visible_transmittance = buidingsync_reader.get_window_data + + # Assertion + expect(window_pane_type).to eq "Single - No LowE - Clear - Aluminum" + expect(fenestration_u_factor).to eq "3.241000" + expect(solar_heat_gain_coefficient).to eq "0.391000" + expect(visible_transmittance).to eq "0.391000" + end + + it "return nil if there are no windows" do + # Set Up + doc = REXML::Document.new wrap_in_systems("") + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + window_pane_type, fenestration_u_factor, solar_heat_gain_coefficient, visible_transmittance = buidingsync_reader.get_window_data + + # Assertion + expect(window_pane_type).to eq nil + end + + # warn not error + it "return nil if all windows are invalid" do + # Set Up + doc = REXML::Document.new wrap_in_systems(<<~XML) + + #{invalid_window} + + XML + + # Action + buidingsync_reader = BOSS::BuildingSyncReader.new(doc, nil, ASHRAE90_1) + window_pane_type, fenestration_u_factor, solar_heat_gain_coefficient, visible_transmittance = buidingsync_reader.get_window_data + + # Assertion + expect(window_pane_type).to eq nil + end + end end