From 7b7dd4a3a3cf338e1c9a78e3c9d0550b18e34c43 Mon Sep 17 00:00:00 2001 From: brgix Date: Mon, 30 Mar 2026 17:54:44 -0400 Subject: [PATCH 1/7] Revisits (interzone) surface air film resistances --- spec/osut_tests_spec.rb | 453 +++++++++++++++++++++++++++++----------- 1 file changed, 326 insertions(+), 127 deletions(-) diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index eb1c24a..6652565 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -676,133 +676,6 @@ expect(mod1.debug?).to be true expect(mod1.logs.size).to eq(1) expect(mod1.logs.first[:message]).to eq(m3) - - # PlanarSurface class method 'filmResistance' reports standard interior or - # exterior air film resistances (ref: ASHRAE Fundamentals), e.g.: - types = {} - types["StillAir_HorizontalSurface_HeatFlowsUpward" ] = 0.107 - types["StillAir_45DegreeSurface_HeatFlowsUpward" ] = 0.109 - types["StillAir_VerticalSurface" ] = 0.120 - types["StillAir_45DegreeSurface_HeatFlowsDownward" ] = 0.134 - types["StillAir_HorizontalSurface_HeatFlowsDownward"] = 0.162 - types["MovingAir_15mph" ] = 0.030 - types["MovingAir_7p5mph" ] = 0.044 - # https://github.com/NREL/OpenStudio/blob/ - # 1c6fe48c49987c16e95e90ee3bd088ad0649ab9c/src/model/ - # PlanarSurface.cpp#L854 - - OpenStudio::Model::FilmResistanceType.getValues.each do |i| - t1 = OpenStudio::Model::FilmResistanceType.new(i) - t2 = OpenStudio::Model::FilmResistanceType.new(types.keys.at(i)) - r = OpenStudio::Model::PlanarSurface.filmResistance(t1) - expect(t1).to eq(t2) - expect(r).to be_within(0.001).of(types.values.at(i)) - next if i > 4 - - # PlanarSurface class method 'stillAirFilmResistance' offers a - # tilt-dependent interior air film resistance, e.g.: - deg = i * 45 - rad = deg * Math::PI/180 - rsi = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(rad) - # puts "#{i}: #{deg}: #{r}: #{rsi}" - # 0: 0: 0.107: 0.106 - # 1: 45: 0.109: 0.109 # ... OK - # 2: 90: 0.120: 0.120 # ... OK - # 3: 135: 0.134: 0.137 - # 4: 180: 0.162: 0.160 - next if deg < 45 || deg > 90 - - expect(rsi).to be_within(0.001).of(r) - # The method is used for (opaque) Surfaces. The correlation/regression - # isn't perfect, yet appears fairly reliable for intermediate angles - # between ~0° and 90°. - # https://github.com/NREL/OpenStudio/blob/ - # 1c6fe48c49987c16e95e90ee3bd088ad0649ab9c/src/model/ - # PlanarSurface.cpp#L878 - end - end - - it "checks rsi calculations" do - translator = OpenStudio::OSVersion::VersionTranslator.new - expect(mod1.clean!).to eq(DBG) - - file = File.join(__dir__, "files/osms/out/seb2.osm") - path = OpenStudio::Path.new(file) - model = translator.loadModel(path) - expect(model).to_not be_empty - model = model.get - - m = "OSut::rsi" - m1 = "Invalid 'lc' arg #1 (#{m})" - m2 = "Negative 'film' (#{m})" - m3 = "'film' NilClass? expecting Numeric (#{m})" - m4 = "Negative 'temp K' (#{m})" - m5 = "'temp K' NilClass? expecting Numeric (#{m})" - - model.getSurfaces.each do |s| - next unless s.isPartOfEnvelope - - lc = s.construction - expect(lc).to_not be_empty - lc = lc.get.to_LayeredConstruction - expect(lc).to_not be_empty - lc = lc.get - - if s.isGroundSurface # 4x slabs on grade in SEB model - expect(s.filmResistance).to be_within(TOL).of(0.160) - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.448) - expect(mod1.status).to be_zero - else - if s.surfaceType == "Wall" - expect(s.filmResistance).to be_within(TOL).of(0.150) - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(2.616) - expect(mod1.status).to be_zero - else # RoofCeiling - expect(s.filmResistance).to be_within(TOL).of(0.136) - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(5.631) - expect(mod1.status).to be_zero - end - end - end - - expect(mod1.rsi("", 0.150)).to be_within(TOL).of(0) - expect(mod1.debug?).to be true - expect(mod1.logs.size).to eq(1) - expect(mod1.logs.first[:message]).to eq(m1) - - expect(mod1.clean!).to eq(DBG) - expect(mod1.rsi(nil, 0.150)).to be_within(TOL).of(0) - expect(mod1.debug?).to be true - expect(mod1.logs.size).to eq(1) - expect(mod1.logs.first[:message]).to eq(m1) - - lc = model.getLayeredConstructionByName("SLAB-ON-GRADE-FLOOR") - expect(lc).to_not be_empty - lc = lc.get - - expect(mod1.clean!).to eq(DBG) - expect(mod1.rsi(lc, -1)).to be_within(TOL).of(0) - expect(mod1.error?).to be true - expect(mod1.logs.size).to eq(1) - expect(mod1.logs.first[:message]).to eq(m2) - - expect(mod1.clean!).to eq(DBG) - expect(mod1.rsi(lc, nil)).to be_within(TOL).of(0) - expect(mod1.debug?).to be true - expect(mod1.logs.size).to eq(1) - expect(mod1.logs.first[:message]).to eq(m3) - - expect(mod1.clean!).to eq(DBG) - expect(mod1.rsi(lc, 0.150, -300)).to be_within(TOL).of(0) - expect(mod1.error?).to be true - expect(mod1.logs.size).to eq(1) - expect(mod1.logs.first[:message]).to eq(m4) - - expect(mod1.clean!).to eq(DBG) - expect(mod1.rsi(lc, 0.150, nil)).to be_within(TOL).of(0) - expect(mod1.debug?).to be true - expect(mod1.logs.size).to eq(1) - expect(mod1.logs.first[:message]).to eq(m5) end it "checks (opaque) insulating layers within a layered construction" do @@ -5499,6 +5372,332 @@ module M expect(mod1.facets(spaces).size).to eq(surfs.size + subs.size) end + it "checks rsi calculations" do + translator = OpenStudio::OSVersion::VersionTranslator.new + expect(mod1.clean!).to eq(DBG) + + # PlanarSurface method 'filmResistance' reports standard interior or + # exterior air film resistances for DISCRETE tilts, per ASHRAE Fundamentals. + # https://github.com/NatLabRockies/OpenStudio/blob/ + # 8008ef767fdc0f9d3dd3fabd383da15d009aef76/src/model/ + # PlanarSurface.cpp#L843 + + # Surface type identifiers to fetch filmResistance values. + fts = {} + fts["STILLAIR_HORIZONTALSURFACE_HEATFLOWSUPWARD" ] = 0.107427212046 + fts["STILLAIR_45DEGREESURFACE_HEATFLOWSUPWARD" ] = 0.109188313883 + fts["STILLAIR_VERTICALSURFACE" ] = 0.119754924904 + fts["STILLAIR_45DEGREESURFACE_HEATFLOWSDOWNWARD" ] = 0.133843739599 + fts["STILLAIR_HORIZONTALSURFACE_HEATFLOWSDOWNWARD"] = 0.162021368988 + fts["MOVINGAIR_15MPH" ] = 0.029938731226 + fts["MOVINGAIR_7P5MPH" ] = 0.044027545921 + + OpenStudio::Model::FilmResistanceType.getValues.each do |i| + t1 = OpenStudio::Model::FilmResistanceType.new(i) + t2 = OpenStudio::Model::FilmResistanceType.new(fts.keys.at(i)) + r = OpenStudio::Model::PlanarSurface.filmResistance(t1) + expect(t1).to eq(t2) + expect(r).to be_within(0.001).of(fts.values.at(i)) + next if i > 4 + + # PlanarSurface method 'stillAirFilmResistance' supports a CONTINUOUS + # tilt-dependent interior air film resistance. + # https://github.com/NatLabRockies/OpenStudio/blob/ + # 8008ef767fdc0f9d3dd3fabd383da15d009aef76/src/model/ + # PlanarSurface.cpp#L867 + deg = i * 45 + rad = deg * Math::PI/180 + rsi = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(rad) + per = "%.2f" % (100 * (r - rsi) / r) + # puts "#{i}: #{deg}: #{r.round(5)}: #{rsi.round(5)} #{per} %" + # + # 0: 0: 0.10743: 0.10604 1.29 % horizontal, facing up + # 1: 45: 0.10919: 0.10944 -0.23 % + # 2: 90: 0.11975: 0.11965 0.09 % vertical + # 3: 135: 0.13384: 0.13665 -2.10 % + # 4: 180: 0.16202: 0.16045 0.97 % horizontal, facing down + next if deg < 45 || deg > 90 + + # The method is used for (opaque) Surfaces. The correlation/regression + # isn't perfect, yet appears fairly reliable for intermediate angles + # between ~0° and 90°. + expect(rsi).to be_within(0.001).of(r) + end + + # Surface class method 'filmResistance' is different (than PlanarSurface). + # It reports the sum of interior and exterior surface air film resistances, + # specific to a given surface. + # https://github.com/NatLabRockies/OpenStudio/blob/ + # 8008ef767fdc0f9d3dd3fabd383da15d009aef76/src/model/ + # Surface.cpp#L1400-L1419 + # + # The method relies on 'isPartOfEnvelope', which unfortunately returns + # FALSE for insulated INTERZONE surfaces, e.g.: + # - floors of an UNCONDITIONED attic + # - ceiling of an UNCONDITIONED crawlspace + # + # These are definitely envelope surfaces. + # https://github.com/NatLabRockies/OpenStudio/blob/ + # 8008ef767fdc0f9d3dd3fabd383da15d009aef76/src/model/ + # Surface.cpp#L1243-L1247 + # + # In cases such as insulated INTERZONE surfaces, 'filmResistance' simply + # doubles the reported still air (interior) film resistance. This overshoots + # calculated thermal resistance. Although such an approximation is less of + # an issue when dealing with highly insulated constructions, caution may be + # required when attempting to accommodate key standards like ASHRAE 90.1: + # + # "building envelope" (90.1 2022, 2025): + # "the EXTERIOR plus the SEMIEXTERIOR portions of a building. For the + # purposes of determining building envelope requirements, the + # classifications are defined as follows: + # - EXTERIOR building envelope: the elements of a building that + # separate CONDITIONED spaces from the exterior. + # - SEMIEXTERIOR building envelope: the elements of a building that + # separate CONDITIONED space from UNCONDITIONED space or that enclose + # SEMIHEATED spaces through which thermal energy may be transferred + # to or from the EXTERIOR, to or from UNCONDITIONED spaces, or to or + # from CONDITIONED spaces." + # + # This issue is discussed here: + # https://github.com/NatLabRockies/EnergyPlus/issues/9470 + # + # And in part discussed here: + # https://www.ashrae.org/file%20library/technical%20resources/ + # standards%20and%20guidelines/standards%20intepretations/ + # ic-90.1-2019-8.pdf + + # Testing this outcome, first with an UNCONDITIONED attic case. + expect(mod1.clean!).to eq(DBG) + + file = File.join(__dir__, "files/osms/out/office_attic.osm") + path = OpenStudio::Path.new(file) + model = translator.loadModel(path) + expect(model).to_not be_empty + model = model.get + + attic = model.getSpaceByName("Attic") + expect(attic).to_not be_empty + attic = attic.get + expect(mod1.unconditioned?(attic)).to be true + + # Test attic (insulated) floors, then their adjacent ceilings. + model.getSurfaces.each do |surface| + next unless surface.surfaceType.downcase == "floor" + next unless surface.outsideBoundaryCondition.downcase == "surface" + + id = surface.nameString + tilt = surface.tilt + expect(id.downcase.include?("attic_floor")) + space = surface.space + expect(space).to_not be_empty + space = space.get + expect(space).to eq(attic) + + # A surface's 'tilt' points outward (from its parent space), e.g. a + # horizontal attic floor faces downward, or 180°. + expect(tilt.round(4)).to eq(Math::PI.round(4)) + + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance + expect(r1.round(3)).to eq(0.321) + expect(r2.round(3)).to eq(0.321) + + # Test adjacent ceilings. + ceiling = surface.adjacentSurface + expect(ceiling).to_not be_empty + ceiling = ceiling.get + + nom = ceiling.nameString + tilt = ceiling.tilt + expect(nom.downcase.include?("_ceiling")) + + # A horizontal ceiling faces upward, or 0°. + expect(tilt.round(4)).to eq(0) + + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = ceiling.filmResistance + expect(r1.round(3)).to eq(0.212) # not 0.321! + expect(r2.round(3)).to eq(0.212) # not 0.321! + + # OS-reported film resistances: 0.212 vs 0.321 - which one should apply? + # + # ---------------------------------------------------------------------- + # FYI, EnergyPlus reported (standard condition) U-factors: + # + # - attic floors: + # - U with film: 0.151 (R with film: 6.623) + # - U without film: 0.158 (R without film: 6.329) + # TOTAL film resistance = 0.267 ? + # + # - adjacent ceilings below: + # - U with film: 0.151 (R with film: 6.623) + # - U without film: 0.158 (R without film: 6.329) + # TOTAL film resistance = 0.267 ! + # + # Reminder: + # fts["STILLAIR_HORIZONTALSURFACE_HEATFLOWSUPWARD" ] = ~0.107 + # fts["STILLAIR_HORIZONTALSURFACE_HEATFLOWSDOWNWARD"] = ~0.162 + # + # ... the sum of both = 0.269 (pretty close) + # + # Similarly: + # OpenStudio::Model::PlanarSurface.stillAirFilmResistance( 0°) = 0.106 + # OpenStudio::Model::PlanarSurface.stillAirFilmResistance(180°) = 0.160 + # + # ... the sum of both = 0.266 (even closer) + # + # Regardless of how EnergyPlus determines combined film resistances, they + # are reported consistently, from either adjacent surface. + end + + # Skylight well walls? + attic.surfaces.each do |surface| + next unless surface.surfaceType.downcase == "wall" + + tilt = surface.tilt + expect(surface.outsideBoundaryCondition.downcase).to eq("surface") + + # Skylight well walls are vertical. + expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance + expect(r1.round(3)).to eq(0.239) + expect(r2.round(3)).to eq(0.239) + + # Adjacent skylight well walls? + adjacent = surface.adjacentSurface + expect(adjacent).to_not be_empty + adjacent = adjacent.get + id = adjacent.nameString + + expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance + expect(r1.round(3)).to eq(0.239) + expect(r2.round(3)).to eq(0.239) + end + + # Different from interzone walls in CONDITIONED, occupied spaces? + model.getSpaces.each do |space| + next if space == attic + + space.surfaces.each do |surface| + next unless surface.surfaceType.downcase == "wall" + next unless surface.outsideBoundaryCondition == "surface" + + tilt = surface.tilt + expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance + expect(r1.round(3)).to eq(0.239) # same as skylight well walls. + expect(r2.round(3)).to eq(0.239) + end + end + + # EnergyPlus reported film resistances for INTERZONE walls. + # - insulated INTERZONE skylight well walls: + # - U with film: 0.292 (R with film: 3.425) + # - U without film: 0.314 (R without film: 3.185) + # TOTAL film resistance = 0.240 (same as OpenStudio) + # + # ... vs other INTERZONE walls: + # - U with film: 2.511 (R with film: 0.398) + # - U without film: 6.299 (R without film: 0.159) + # TOTAL film resistance = 0.239 (same as OpenStudio) + + # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # + # Repeat for plenum cases. + file = File.join(__dir__, "files/osms/out/seb2.osm") + path = OpenStudio::Path.new(file) + model = translator.loadModel(path) + expect(model).to_not be_empty + model = model.get + + m = "OSut::rsi" + m1 = "Invalid 'lc' arg #1 (#{m})" + m2 = "Negative 'film' (#{m})" + m3 = "'film' NilClass? expecting Numeric (#{m})" + m4 = "Negative 'temp K' (#{m})" + m5 = "'temp K' NilClass? expecting Numeric (#{m})" + + model.getSurfaces.each do |s| + id = s.nameString + lc = s.construction + expect(lc).to_not be_empty + lc = lc.get.to_LayeredConstruction + expect(lc).to_not be_empty + lc = lc.get + + if s.isPartOfEnvelope # i.e. outdoor-facing or ground-facing only + if s.isGroundSurface # 4x slabs on grade in SEB model + expect(s.filmResistance).to be_within(TOL).of(0.160) + expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.448) + expect(mod1.status).to be_zero + elsif s.surfaceType == "Wall" + expect(s.filmResistance).to be_within(TOL).of(0.150) + expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(2.616) + expect(mod1.status).to be_zero + else # RoofCeiling + expect(s.filmResistance).to be_within(TOL).of(0.136) + expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(5.631) + expect(mod1.status).to be_zero + end + else + expect(s.outsideBoundaryCondition.downcase).to eq("surface") + + if s.surfaceType.downcase == "wall" + expect(s.filmResistance.round(3)).to eq(0.239) # as above walls + elsif s.surfaceType.downcase == "roofceiling" + expect(s.filmResistance.round(3)).to eq(0.212) # as interzone ceilings + else + expect(s.surfaceType.downcase).to eq("floor") + expect(s.filmResistance.round(3)).to eq(0.321) # as attic floors + end + end + end + + # Testing 'rsi' method. Invalid cases. + expect(mod1.rsi("", 0.150)).to be_within(TOL).of(0) + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m1) + + expect(mod1.clean!).to eq(DBG) + expect(mod1.rsi(nil, 0.150)).to be_within(TOL).of(0) + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m1) + + lc = model.getLayeredConstructionByName("SLAB-ON-GRADE-FLOOR") + expect(lc).to_not be_empty + lc = lc.get + + expect(mod1.clean!).to eq(DBG) + expect(mod1.rsi(lc, -1)).to be_within(TOL).of(0) + expect(mod1.error?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m2) + + expect(mod1.clean!).to eq(DBG) + expect(mod1.rsi(lc, nil)).to be_within(TOL).of(0) + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m3) + + expect(mod1.clean!).to eq(DBG) + expect(mod1.rsi(lc, 0.150, -300)).to be_within(TOL).of(0) + expect(mod1.error?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m4) + + expect(mod1.clean!).to eq(DBG) + expect(mod1.rsi(lc, 0.150, nil)).to be_within(TOL).of(0) + expect(mod1.debug?).to be true + expect(mod1.logs.size).to eq(1) + expect(mod1.logs.first[:message]).to eq(m5) + end + it "checks slab generation" do expect(mod1.reset(DBG)).to eq(DBG) expect(mod1.level).to eq(DBG) From ceeb69189cb920ed077f02f5c2fa47324bdf8a22 Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 31 Mar 2026 07:50:03 -0400 Subject: [PATCH 2/7] Adds interzone 'ceiling' category --- lib/osut/utils.rb | 57 +++++++++++++++++++++++++++++++---------- spec/osut_tests_spec.rb | 55 +++++++++++++++++---------------------- 2 files changed, 68 insertions(+), 44 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 2e96f59..daf2c8b 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -106,26 +106,28 @@ module OSut # default inside + outside air film resistances (m2.K/W) @@film = { shading: 0.000, # NA - partition: 0.150, # uninsulated wood- or steel-framed wall - wall: 0.150, # un/insulated wall - roof: 0.140, # un/insulated roof - floor: 0.190, # un/insulated (exposed) floor - basement: 0.120, # un/insulated basement wall - slab: 0.160, # un/insulated basement slab or slab-on-grade + ceiling: 0.267, # interzone floor/ceiling + partition: 0.240, # interzone wall partition + wall: 0.150, # exposed wall + roof: 0.135, # exposed roof + floor: 0.192, # exposed floor + basement: 0.120, # basement wall + slab: 0.162, # basement slab or slab-on-grade door: 0.150, # standard, 45mm insulated steel (opaque) door window: 0.150, # vertical fenestration, e.g. glazed doors, windows - skylight: 0.140 # e.g. domed 4' x 4' skylight + skylight: 0.135 # e.g. domed 4' x 4' skylight }.freeze # default (~1980s) envelope Uo (W/m2•K), based on surface type @@uo = { shading: nil, # N/A + ceiling: nil, # N/A partition: nil, # N/A wall: 0.384, # rated R14.8 hr•ft2F/Btu roof: 0.327, # rated R17.6 hr•ft2F/Btu floor: 0.317, # rated R17.9 hr•ft2F/Btu (exposed floor) - basement: nil, - slab: nil, + basement: nil, # N/A + slab: nil, # N/A door: 1.800, # insulated, unglazed steel door (single layer) window: 2.800, # e.g. patio doors (simple glazing) skylight: 3.500 # all skylight technologies @@ -600,10 +602,6 @@ def genConstruction(model = nil, specs = {}) return mismatch("model", model, cl1, mth) unless model.is_a?(cl1) return mismatch("specs", specs, cl2, mth) unless specs.is_a?(cl2) - specs[:id] = "" unless specs.key?(:id) - id = trim(specs[:id]) - id = "OSut:CON:#{specs[:type]}" if id.empty? - if specs.key?(:type) unless @@uo.keys.include?(specs[:type]) return invalid("surface type", mth, 2, ERR) @@ -612,6 +610,10 @@ def genConstruction(model = nil, specs = {}) specs[:type] = :wall end + specs[:id] = "" unless specs.key?(:id) + id = trim(specs[:id]) + id = "OSut:CON:#{specs[:type]}" if id.empty? + specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo) # can be nil u = specs[:uo] @@ -652,6 +654,35 @@ def genConstruction(model = nil, specs = {}) a[:compo][:mat] = @@mats[mt] a[:compo][:d ] = d a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}" + when :ceiling + unless specs[:clad] == :none + mt = :concrete + mt = :material if specs[:clad] == :light + d = 0.015 + d = 0.100 if specs[:clad] == :medium + d = 0.200 if specs[:clad] == :heavy + a[:clad][:mat] = @@mats[mt] + a[:clad][:d ] = d + a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}" + end + + mt = :mineral + mt = :polyiso if specs[:frame] == :medium + mt = :cellulose if specs[:frame] == :heavy + mt = :material unless u + d = 0.100 + d = 0.015 unless u + a[:compo][:mat] = @@mats[mt] + a[:compo][:d ] = d + a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}" + + unless specs[:finish] == :none + mt = :material + d = 0.015 + a[:finish][:mat] = @@mats[mt] + a[:finish][:d ] = d + a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}" + end when :partition unless specs[:clad] == :none d = 0.015 diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 6652565..260402d 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -26,12 +26,12 @@ film = cls1.class_variable_get(:@@film) uo = cls1.class_variable_get(:@@uo) model = OpenStudio::Model::Model.new - uo1 = 2.140 + uo1 = 1.792 uo2 = 0.214 uo3 = 3.566 uo4 = 4.812 uo5 = 3.765 - uo6 = 3.698 + uo6 = 3.767 uo7 = 4.244 uo8 = uo[:door] uo9 = 0.900 @@ -47,16 +47,6 @@ expect(u).to be_within(TOL).of(uo1) expect(surface.layers.first).to eq(surface.layers.last) - # An alternative to (uninsulated) :partition (+inputs, same outcome). - specs = {type: :wall, clad: :none, uo: nil} - surface = cls1.genConstruction(model, specs) - expect(surface).to_not be_nil - expect(surface).to be_a(OpenStudio::Model::LayeredConstruction) - expect(surface.layers.size).to eq(3) - u = 1 / cls1.rsi(surface, film[:wall]) - expect(u).to be_within(TOL).of(uo1) - expect(surface.layers.first).to eq(surface.layers.last) - # Insulated :partition variant. specs = {type: :partition, uo: uo2} surface = cls1.genConstruction(model, specs) @@ -135,7 +125,7 @@ expect(u).to be_within(TOL).of(uo5) expect(surface.layers.first.nameString).to eq("OSut:concrete:200") - # A light (minimal, 1x layer), uninsulated attic roof (alternative: shading). + # A light (minimal, 1x layer), uninsulated attic roof. specs = {type: :roof, uo: nil, clad: :none, finish: :none} surface = cls1.genConstruction(model, specs) expect(surface).to_not be_nil @@ -144,7 +134,7 @@ u = 1 / cls1.rsi(surface, film[:roof]) expect(u).to be_within(TOL).of(uo6) - # Insulated, cathredral ceiling construction (alternative :shading). + # Insulated, cathredral ceiling construction. specs = {type: :roof, uo: uo2} surface = cls1.genConstruction(model, specs) expect(surface).to_not be_nil @@ -187,14 +177,14 @@ u = 1 / cls1.rsi(surface, film[:roof]) expect(u).to be_within(TOL).of(uo6) - # Unfinished, insulated, framed attic floor (blown cellulose). + # Unfinished, insulated, framed attic floor/ceiling (blown cellulose). model = OpenStudio::Model::Model.new - specs = {type: :floor, uo: uo2, frame: :heavy, finish: :none} + specs = {type: :ceiling, uo: uo2, frame: :heavy, finish: :none} surface = cls1.genConstruction(model, specs) expect(surface).to_not be_nil expect(surface).to be_a(OpenStudio::Model::LayeredConstruction) expect(surface.layers.size).to eq(2) - u = 1 / cls1.rsi(surface, film[:floor]) + u = 1 / cls1.rsi(surface, film[:ceiling]) expect(u).to be_within(TOL).of(uo2) expect(surface.layers[1].nameString).to eq("OSut:K0.023:100") @@ -2786,7 +2776,7 @@ module M # Stress tests collinears. m0 = "'n points' String? expecting Integer (OSut::collinears)" - # Invalid case - raise DEBUG message, yet returns valid collinears. + # Invalid case - raise DBG message, yet returns valid collinears. colls = mod1.collinears([p0, p1, p3, p8], "osut") expect(colls).to be_a(OpenStudio::Point3dVector) expect(colls.size).to eq(1) @@ -4971,7 +4961,7 @@ module M # Reset attic default construction set for insulated interzone walls. construction = mod1.genConstruction(model, {type: :partition, uo: 0.3}) - expect(mod1.rsi(construction, 0.150)).to be_within(TOL).of(1/0.3) + expect(mod1.rsi(construction, 0.240)).to be_within(TOL).of(1/0.3) expect(ia_set.setWallConstruction(construction)).to be true expect(mod1.status).to be_zero @@ -5486,16 +5476,17 @@ module M next unless surface.surfaceType.downcase == "floor" next unless surface.outsideBoundaryCondition.downcase == "surface" - id = surface.nameString - tilt = surface.tilt - expect(id.downcase.include?("attic_floor")) space = surface.space expect(space).to_not be_empty space = space.get expect(space).to eq(attic) + id = surface.nameString + expect(id.downcase.include?("attic_floor")) + # A surface's 'tilt' points outward (from its parent space), e.g. a # horizontal attic floor faces downward, or 180°. + tilt = surface.tilt expect(tilt.round(4)).to eq(Math::PI.round(4)) r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 @@ -5508,11 +5499,11 @@ module M expect(ceiling).to_not be_empty ceiling = ceiling.get - nom = ceiling.nameString - tilt = ceiling.tilt + nom = ceiling.nameString expect(nom.downcase.include?("_ceiling")) # A horizontal ceiling faces upward, or 0°. + tilt = ceiling.tilt expect(tilt.round(4)).to eq(0) r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 @@ -5535,6 +5526,9 @@ module M # - U without film: 0.158 (R without film: 6.329) # TOTAL film resistance = 0.267 ! # + # Regardless of how EnergyPlus determines combined film resistances, they + # are reported consistently, from either diection. + # # Reminder: # fts["STILLAIR_HORIZONTALSURFACE_HEATFLOWSUPWARD" ] = ~0.107 # fts["STILLAIR_HORIZONTALSURFACE_HEATFLOWSDOWNWARD"] = ~0.162 @@ -5545,21 +5539,18 @@ module M # OpenStudio::Model::PlanarSurface.stillAirFilmResistance( 0°) = 0.106 # OpenStudio::Model::PlanarSurface.stillAirFilmResistance(180°) = 0.160 # - # ... the sum of both = 0.266 (even closer) - # - # Regardless of how EnergyPlus determines combined film resistances, they - # are reported consistently, from either adjacent surface. + # ... the sum of both = 0.266 (even closer). end # Skylight well walls? attic.surfaces.each do |surface| next unless surface.surfaceType.downcase == "wall" + # Skylight well walls are vertical. tilt = surface.tilt + expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) expect(surface.outsideBoundaryCondition.downcase).to eq("surface") - # Skylight well walls are vertical. - expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance expect(r1.round(3)).to eq(0.239) @@ -5569,9 +5560,10 @@ module M adjacent = surface.adjacentSurface expect(adjacent).to_not be_empty adjacent = adjacent.get - id = adjacent.nameString + tilt = adjacent.tilt expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance expect(r1.round(3)).to eq(0.239) @@ -5588,6 +5580,7 @@ module M tilt = surface.tilt expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance expect(r1.round(3)).to eq(0.239) # same as skylight well walls. From 0af9b8bfd6bc87aa78914b1766b8fd08f60e7536 Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 31 Mar 2026 07:55:00 -0400 Subject: [PATCH 3/7] Updates version --- lib/osut/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/osut/version.rb b/lib/osut/version.rb index ae7c89d..f08e4ab 100644 --- a/lib/osut/version.rb +++ b/lib/osut/version.rb @@ -29,5 +29,5 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. module OSut - VERSION = "0.8.2".freeze + VERSION = "0.8.3".freeze end From de0a25234c7061f3f355044ac87ae06cb547883e Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 31 Mar 2026 15:27:46 -0400 Subject: [PATCH 4/7] New surface air 'film' resistance method --- lib/osut/utils.rb | 17 ++++++++++++++++- spec/osut_tests_spec.rb | 24 +++++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index daf2c8b..dab5f33 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -107,7 +107,7 @@ module OSut @@film = { shading: 0.000, # NA ceiling: 0.267, # interzone floor/ceiling - partition: 0.240, # interzone wall partition + partition: 0.239, # interzone wall partition wall: 0.150, # exposed wall roof: 0.135, # exposed roof floor: 0.192, # exposed floor @@ -199,6 +199,21 @@ module OSut @@mats[:door ][:rho] = 600.000 @@mats[:door ][:cp ] = 1000.000 + ## + # Returns OSut surface air film resistances. + # + # @param [:to_sym] surface type, e.g. :wall + # + # @return [Hash] OSut collection of surface + def film(type = :wall) + return 0.0 unless type.respond_to?(:to_sym) + + type = type.to_s.downcase.to_sym + type = :wall unless @@film.key?(type) + + @@film[type] + end + ## # Validates if every material in a layered construction is standard & opaque. # diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 260402d..539741a 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -5508,8 +5508,10 @@ module M r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = ceiling.filmResistance + r3 = mod1.film(:ceiling) expect(r1.round(3)).to eq(0.212) # not 0.321! expect(r2.round(3)).to eq(0.212) # not 0.321! + expect(r3.round(3)).to eq(0.267) # OS-reported film resistances: 0.212 vs 0.321 - which one should apply? # @@ -5527,7 +5529,7 @@ module M # TOTAL film resistance = 0.267 ! # # Regardless of how EnergyPlus determines combined film resistances, they - # are reported consistently, from either diection. + # are reported consistently, from either direction. # # Reminder: # fts["STILLAIR_HORIZONTALSURFACE_HEATFLOWSUPWARD" ] = ~0.107 @@ -5566,8 +5568,10 @@ module M r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance + r3 = mod1.film(:partition) expect(r1.round(3)).to eq(0.239) expect(r2.round(3)).to eq(0.239) + expect(r3.round(3)).to eq(0.239) end # Different from interzone walls in CONDITIONED, occupied spaces? @@ -5583,8 +5587,10 @@ module M r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance + r3 = mod1.film(:partition) expect(r1.round(3)).to eq(0.239) # same as skylight well walls. expect(r2.round(3)).to eq(0.239) + expect(r3.round(3)).to eq(0.239) end end @@ -5592,7 +5598,7 @@ module M # - insulated INTERZONE skylight well walls: # - U with film: 0.292 (R with film: 3.425) # - U without film: 0.314 (R without film: 3.185) - # TOTAL film resistance = 0.240 (same as OpenStudio) + # TOTAL film resistance = 0.240 (~same as OpenStudio) # # ... vs other INTERZONE walls: # - U with film: 2.511 (R with film: 0.398) @@ -5626,31 +5632,39 @@ module M if s.isGroundSurface # 4x slabs on grade in SEB model expect(s.filmResistance).to be_within(TOL).of(0.160) expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.448) - expect(mod1.status).to be_zero + expect(mod1.rsi(lc, mod1.film(:slab))).to be_within(TOL).of(0.448) elsif s.surfaceType == "Wall" expect(s.filmResistance).to be_within(TOL).of(0.150) expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(2.616) - expect(mod1.status).to be_zero + expect(mod1.rsi(lc, mod1.film(:wall))).to be_within(TOL).of(2.616) else # RoofCeiling expect(s.filmResistance).to be_within(TOL).of(0.136) expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(5.631) - expect(mod1.status).to be_zero + expect(mod1.rsi(lc, mod1.film(:roof))).to be_within(TOL).of(5.631) end else expect(s.outsideBoundaryCondition.downcase).to eq("surface") if s.surfaceType.downcase == "wall" + # puts "#{s.nameString} : #{mod1.rsi(lc, s.filmResistance)}" 0.6798199211260842 + expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.680) + expect(mod1.rsi(lc, mod1.film(:partition))).to be_within(TOL).of(0.680) expect(s.filmResistance.round(3)).to eq(0.239) # as above walls elsif s.surfaceType.downcase == "roofceiling" expect(s.filmResistance.round(3)).to eq(0.212) # as interzone ceilings + expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.331) + expect(mod1.rsi(lc, mod1.film(:ceiling))).to be_within(TOL).of(0.386) else expect(s.surfaceType.downcase).to eq("floor") expect(s.filmResistance.round(3)).to eq(0.321) # as attic floors + expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.440) + expect(mod1.rsi(lc, mod1.film(:ceiling))).to be_within(TOL).of(0.386) end end end # Testing 'rsi' method. Invalid cases. + expect(mod1.status).to be_zero expect(mod1.rsi("", 0.150)).to be_within(TOL).of(0) expect(mod1.debug?).to be true expect(mod1.logs.size).to eq(1) From 9468bc60fbe44cd998cbe07d094aac7e0bcde0c7 Mon Sep 17 00:00:00 2001 From: brgix Date: Thu, 2 Apr 2026 08:04:19 -0400 Subject: [PATCH 5/7] Adds new 'filmResistances' method --- lib/osut/utils.rb | 56 +++++++++++++++++++++---- spec/osut_tests_spec.rb | 93 ++++++++++++++++++++++++++++------------- 2 files changed, 113 insertions(+), 36 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index dab5f33..5854c2a 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -106,7 +106,7 @@ module OSut # default inside + outside air film resistances (m2.K/W) @@film = { shading: 0.000, # NA - ceiling: 0.267, # interzone floor/ceiling + ceiling: 0.266, # interzone floor/ceiling partition: 0.239, # interzone wall partition wall: 0.150, # exposed wall roof: 0.135, # exposed roof @@ -200,18 +200,58 @@ module OSut @@mats[:door ][:cp ] = 1000.000 ## - # Returns OSut surface air film resistances. + # Returns surface air film resistance(s). Surface tilt-dependent values are + # returned if a valid surface tilt [0, PI] is provided. Otherwise, generic + # tilt-independent air film resistances are returned instead. # - # @param [:to_sym] surface type, e.g. :wall + # @param [:to_sym] surface type, e.g. :roof, :wall, :partition, :ceiling + # @param [Numeric] surface tilt (in rad), optional # - # @return [Hash] OSut collection of surface - def film(type = :wall) - return 0.0 unless type.respond_to?(:to_sym) + # @return [Float] surface air film resistance(s) + # @return [0.0] if invalid input (see logs) + def filmResistances(type = :wall, tilt = 2 * Math::PI) + mth = "OSut::#{__callee__}" + + unless tilt.is_a?(Numeric) + return mismatch("tilt", tilt, Float, mth, DBG, 0.0) + end + + unless type.respond_to?(:to_sym) + return mismatch("type", type, Symbol, mth, DBG, 0.0) + end type = type.to_s.downcase.to_sym - type = :wall unless @@film.key?(type) - @@film[type] + unless @@film.key?(type) + return invalid("type", mth, 1, DBG, 0.0) + end + + # Generic, tilt-independent values. + r = @@film[type] + return r if type == :shading + + # Valid tilt? + if tilt.between?(0, Math::PI) + r = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) + return r if type == :basement || type == :slab + + if type == :ceiling || type == :partition + # Interzone. Fetch reciprocal tilt, e.g. if tilt == 0°, tiltx = 180° + tiltx = tilt + Math::PI + + # Assuming tilt is contrained [0°, 180°] - constrain tiltx [0° 180°]: + # e.g. tiltx == 210° if tilt == 30°, so convert tiltx to 150° + # e.g. tiltx == 330° if tilt == 150°, so convert tiltx to 30° + # e.g. tiltx == 275° if tilt == 95°, so convert tiltx to 85° + tiltx = Math::PI - tilt if tiltx > Math::PI + + r += OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tiltx) + else + r += 0.03 # "MOVINGAIR_15MPH" + end + end + + r end ## diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 539741a..442aaa7 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -5431,11 +5431,12 @@ module M # 8008ef767fdc0f9d3dd3fabd383da15d009aef76/src/model/ # Surface.cpp#L1243-L1247 # - # In cases such as insulated INTERZONE surfaces, 'filmResistance' simply - # doubles the reported still air (interior) film resistance. This overshoots - # calculated thermal resistance. Although such an approximation is less of - # an issue when dealing with highly insulated constructions, caution may be - # required when attempting to accommodate key standards like ASHRAE 90.1: + # For INTERZONE surfaces, 'filmResistance' simply doubles the reported still + # air (interior) film resistance. The solution either underestimates or + # overestimates calculated surface air film resistances for non-vertical + # surfaces. Although such an approximation is less of an issue when dealing + # with highly insulated constructions, caution may be required when + # attempting to accommodate key standards like ASHRAE 90.1: # # "building envelope" (90.1 2022, 2025): # "the EXTERIOR plus the SEMIEXTERIOR portions of a building. For the @@ -5508,10 +5509,12 @@ module M r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = ceiling.filmResistance - r3 = mod1.film(:ceiling) + r3 = mod1.filmResistances(:ceiling) + r4 = mod1.filmResistances(:ceiling, tilt) expect(r1.round(3)).to eq(0.212) # not 0.321! expect(r2.round(3)).to eq(0.212) # not 0.321! - expect(r3.round(3)).to eq(0.267) + expect(r3.round(3)).to eq(0.266) + expect(r4.round(3)).to eq(0.266) # OS-reported film resistances: 0.212 vs 0.321 - which one should apply? # @@ -5568,10 +5571,12 @@ module M r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance - r3 = mod1.film(:partition) + r3 = mod1.filmResistances(:partition) + r4 = mod1.filmResistances(:partition, tilt) expect(r1.round(3)).to eq(0.239) expect(r2.round(3)).to eq(0.239) expect(r3.round(3)).to eq(0.239) + expect(r4.round(3)).to eq(0.239) end # Different from interzone walls in CONDITIONED, occupied spaces? @@ -5587,10 +5592,12 @@ module M r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance - r3 = mod1.film(:partition) + r3 = mod1.filmResistances(:partition) + r4 = mod1.filmResistances(:partition, tilt) expect(r1.round(3)).to eq(0.239) # same as skylight well walls. expect(r2.round(3)).to eq(0.239) expect(r3.round(3)).to eq(0.239) + expect(r3.round(4)).to eq(0.239) end end @@ -5627,38 +5634,68 @@ module M lc = lc.get.to_LayeredConstruction expect(lc).to_not be_empty lc = lc.get + r1 = s.filmResistance if s.isPartOfEnvelope # i.e. outdoor-facing or ground-facing only if s.isGroundSurface # 4x slabs on grade in SEB model - expect(s.filmResistance).to be_within(TOL).of(0.160) - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.448) - expect(mod1.rsi(lc, mod1.film(:slab))).to be_within(TOL).of(0.448) + r2 = mod1.filmResistances(:slab) + r3 = mod1.filmResistances(:slab, s.tilt) + expect(r1).to be_within(TOL).of(0.160) + expect(r2).to be_within(TOL).of(0.160) + expect(r3).to be_within(TOL).of(0.160) + expect(mod1.rsi(lc, r1)).to be_within(TOL).of(0.448) + expect(mod1.rsi(lc, r2)).to be_within(TOL).of(0.448) + expect(mod1.rsi(lc, r3)).to be_within(TOL).of(0.448) elsif s.surfaceType == "Wall" - expect(s.filmResistance).to be_within(TOL).of(0.150) - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(2.616) - expect(mod1.rsi(lc, mod1.film(:wall))).to be_within(TOL).of(2.616) + r2 = mod1.filmResistances(:wall) + r3 = mod1.filmResistances(:wall, s.tilt) + expect(r1).to be_within(TOL).of(0.150) + expect(r2).to be_within(TOL).of(0.150) + expect(r3).to be_within(TOL).of(0.150) + expect(mod1.rsi(lc, r1)).to be_within(TOL).of(2.616) + expect(mod1.rsi(lc, r2)).to be_within(TOL).of(2.616) + expect(mod1.rsi(lc, r3)).to be_within(TOL).of(2.616) else # RoofCeiling - expect(s.filmResistance).to be_within(TOL).of(0.136) - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(5.631) - expect(mod1.rsi(lc, mod1.film(:roof))).to be_within(TOL).of(5.631) + r2 = mod1.filmResistances(:roof) + r3 = mod1.filmResistances(:roof, s.tilt) + expect(r1).to be_within(TOL).of(0.136) + expect(r2).to be_within(TOL).of(0.136) + expect(r3).to be_within(TOL).of(0.136) + expect(mod1.rsi(lc, r1)).to be_within(TOL).of(5.631) + expect(mod1.rsi(lc, r2)).to be_within(TOL).of(5.631) + expect(mod1.rsi(lc, r3)).to be_within(TOL).of(5.631) end else expect(s.outsideBoundaryCondition.downcase).to eq("surface") if s.surfaceType.downcase == "wall" - # puts "#{s.nameString} : #{mod1.rsi(lc, s.filmResistance)}" 0.6798199211260842 - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.680) - expect(mod1.rsi(lc, mod1.film(:partition))).to be_within(TOL).of(0.680) - expect(s.filmResistance.round(3)).to eq(0.239) # as above walls + r2 = mod1.filmResistances(:partition) + r3 = mod1.filmResistances(:partition, s.tilt) + expect(r1.round(3)).to eq(0.239) # as above walls + expect(r2.round(3)).to eq(0.239) + expect(r3.round(3)).to eq(0.239) + expect(mod1.rsi(lc, r1)).to be_within(TOL).of(0.680) + expect(mod1.rsi(lc, r2)).to be_within(TOL).of(0.680) + expect(mod1.rsi(lc, r3)).to be_within(TOL).of(0.680) elsif s.surfaceType.downcase == "roofceiling" - expect(s.filmResistance.round(3)).to eq(0.212) # as interzone ceilings - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.331) - expect(mod1.rsi(lc, mod1.film(:ceiling))).to be_within(TOL).of(0.386) + r2 = mod1.filmResistances(:ceiling) + r3 = mod1.filmResistances(:ceiling, s.tilt) + expect(r1.round(3)).to eq(0.212) # as interzone ceilings + expect(r2.round(3)).to eq(0.266) + expect(r3.round(3)).to eq(0.266) + expect(mod1.rsi(lc, r1)).to be_within(TOL).of(0.331) + expect(mod1.rsi(lc, r2)).to be_within(TOL).of(0.386) + expect(mod1.rsi(lc, r3)).to be_within(TOL).of(0.386) else expect(s.surfaceType.downcase).to eq("floor") - expect(s.filmResistance.round(3)).to eq(0.321) # as attic floors - expect(mod1.rsi(lc, s.filmResistance)).to be_within(TOL).of(0.440) - expect(mod1.rsi(lc, mod1.film(:ceiling))).to be_within(TOL).of(0.386) + r2 = mod1.filmResistances(:ceiling) + r3 = mod1.filmResistances(:ceiling, s.tilt) + expect(r1.round(3)).to eq(0.321) # as attic floors + expect(r2.round(3)).to eq(0.266) + expect(r3.round(3)).to eq(0.266) + expect(mod1.rsi(lc, r1)).to be_within(TOL).of(0.440) + expect(mod1.rsi(lc, r2)).to be_within(TOL).of(0.386) + expect(mod1.rsi(lc, r3)).to be_within(TOL).of(0.386) end end end From 3183e94b5ba21b20b278c76c2952f07ce76f630c Mon Sep 17 00:00:00 2001 From: brgix Date: Mon, 13 Apr 2026 15:10:15 -0400 Subject: [PATCH 6/7] Adds final edits --- lib/osut/utils.rb | 2 +- spec/osut_tests_spec.rb | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 5854c2a..4857661 100644 --- a/lib/osut/utils.rb +++ b/lib/osut/utils.rb @@ -527,7 +527,7 @@ def assignUniqueMaterial(lc = nil, index = nil) ## # Resets a construction's Uo factor by adjusting its insulating layer # thermal conductivity, then if needed its thickness (or its RSi value if - # massless). Unless material uniquness is requested, a matching material is + # massless). Unless material uniqueness is requested, a matching material is # recovered instead of instantiating a new one. The latter is renamed # according to its adjusted conductivity/thickness (or RSi value). # diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index 442aaa7..f18887d 100644 --- a/spec/osut_tests_spec.rb +++ b/spec/osut_tests_spec.rb @@ -4960,8 +4960,9 @@ module M expect(ratio.round(2)).to eq(srr) # Reset attic default construction set for insulated interzone walls. + filmRSI = mod1.filmResistances(:partition) construction = mod1.genConstruction(model, {type: :partition, uo: 0.3}) - expect(mod1.rsi(construction, 0.240)).to be_within(TOL).of(1/0.3) + expect(mod1.rsi(construction, filmRSI).round(3)).to eq((1/0.3).round(3)) expect(ia_set.setWallConstruction(construction)).to be true expect(mod1.status).to be_zero @@ -5492,8 +5493,12 @@ module M r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 r2 = surface.filmResistance + r3 = mod1.filmResistances(:ceiling) + r4 = mod1.filmResistances(:ceiling, tilt) expect(r1.round(3)).to eq(0.321) expect(r2.round(3)).to eq(0.321) + expect(r3.round(3)).to eq(0.266) + expect(r4.round(3)).to eq(0.266) # Test adjacent ceilings. ceiling = surface.adjacentSurface @@ -5513,8 +5518,8 @@ module M r4 = mod1.filmResistances(:ceiling, tilt) expect(r1.round(3)).to eq(0.212) # not 0.321! expect(r2.round(3)).to eq(0.212) # not 0.321! - expect(r3.round(3)).to eq(0.266) - expect(r4.round(3)).to eq(0.266) + expect(r3.round(3)).to eq(0.266) # same as above + expect(r4.round(3)).to eq(0.266) # same as above # OS-reported film resistances: 0.212 vs 0.321 - which one should apply? # From b471fcbca3d16c283ea502d0ab96d901d11c9db2 Mon Sep 17 00:00:00 2001 From: brgix Date: Tue, 14 Apr 2026 07:17:06 -0400 Subject: [PATCH 7/7] Bumps up (minor) version --- lib/osut/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/osut/version.rb b/lib/osut/version.rb index f08e4ab..e45b093 100644 --- a/lib/osut/version.rb +++ b/lib/osut/version.rb @@ -29,5 +29,5 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. module OSut - VERSION = "0.8.3".freeze + VERSION = "0.9.0".freeze end