diff --git a/lib/osut/utils.rb b/lib/osut/utils.rb index 2e96f59..4857661 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.266, # interzone floor/ceiling + partition: 0.239, # 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 @@ -197,6 +199,61 @@ module OSut @@mats[:door ][:rho] = 600.000 @@mats[:door ][:cp ] = 1000.000 + ## + # 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. :roof, :wall, :partition, :ceiling + # @param [Numeric] surface tilt (in rad), optional + # + # @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 + + 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 + ## # Validates if every material in a layered construction is standard & opaque. # @@ -470,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). # @@ -600,10 +657,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 +665,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 +709,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/lib/osut/version.rb b/lib/osut/version.rb index ae7c89d..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.2".freeze + VERSION = "0.9.0".freeze end diff --git a/spec/osut_tests_spec.rb b/spec/osut_tests_spec.rb index eb1c24a..f18887d 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") @@ -676,133 +666,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 @@ -2913,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) @@ -5097,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.150)).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 @@ -5499,6 +5363,390 @@ 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 + # + # 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 + # 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" + + 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 + 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 + expect(ceiling).to_not be_empty + ceiling = ceiling.get + + 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 + r2 = ceiling.filmResistance + 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.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? + # + # ---------------------------------------------------------------------- + # 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 ! + # + # Regardless of how EnergyPlus determines combined film resistances, they + # are reported consistently, from either direction. + # + # 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). + 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") + + 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 + + tilt = adjacent.tilt + expect(tilt.round(4)).to eq((Math::PI / 2).round(4)) + + r1 = OpenStudio::Model::PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance + 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? + 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 + 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 + + # 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 + r1 = s.filmResistance + + if s.isPartOfEnvelope # i.e. outdoor-facing or ground-facing only + if s.isGroundSurface # 4x slabs on grade in SEB model + 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" + 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 + 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" + 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" + 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") + 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 + + # 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) + 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)