diff --git a/pyproject.toml b/pyproject.toml index 7bf9dbe..fea21af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "osut" -version = "0.8.2" +version = "0.9.0" description = "OpenStudio SDK utilities for Python" readme = "README.md" requires-python = ">=3.2" diff --git a/src/osut/osut.py b/src/osut/osut.py index 582df1e..7f2f7bf 100644 --- a/src/osut/osut.py +++ b/src/osut/osut.py @@ -103,20 +103,22 @@ class _CN: # Default inside + outside air film resistances (m2.K/W). _film = dict( shading = 0.000, # NA - partition = 0.150, # uninsulated wood- or steel-framed wall + ceiling = 0.266, # interzone floor/ceiling + partition = 0.239, # interzone wall partition wall = 0.150, # un/insulated wall - roof = 0.140, # un/insulated roof - floor = 0.190, # un/insulated (exposed) floor + roof = 0.135, # un/insulated roof + floor = 0.192, # un/insulated (exposed) floor basement = 0.120, # un/insulated basement wall - slab = 0.160, # un/insulated basement slab or slab-on-grade + slab = 0.162, # un/insulated 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 ) # Default (~1980s) envelope Uo (W/m2•K), based on surface type. _uo = dict( shading = None, # N/A + ceiling = None, # N/A partition = None, # N/A wall = 0.384, # rated R14.8 hr•ft2F/Btu roof = 0.327, # rated R17.6 hr•ft2F/Btu @@ -309,6 +311,64 @@ def clamp(value, minimum, maximum) -> float: return value +def filmResistances(type="wall", tilt=2*math.pi) -> float: + """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. + + Args: + type (string): + Surface type, e.g. "roof", "wall", "partition", "ceiling" + tilt (float): + Surface tilt (in rad), optional + + Returns: + float: Surface air film resistance(s) + 0.0: If invalid inputs (see logs). + + """ + mth = "osut.filmResistances" + + try: + tilt = float(tilt) + except: + return oslg.mismatch("surface tilt", tilt, float, mth, CN.DBG, 0.0) + + try: + type = str(type) + except: + return oslg.mismatch("surface type", type, str, mth, CN.DBG, 0.0) + + if type not in film(): + return oslg.invalid("surface type", mth, 1, CN.DBG, 0.0) + + # Generic, tilt-independent values. + r = film()[type] + + if type == "shading": + return r + elif 0.0 <= tilt <= math.pi: + r = openstudio.model.PlanarSurface.stillAirFilmResistance(tilt) + + if type == "basement" or type == "slab": + return r + elif type == "ceiling" or 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° + if tiltx > math.pi: tiltx = math.pi - tilt + + r += openstudio.model.PlanarSurface.stillAirFilmResistance(tiltx) + else: + r += 0.03 # "MOVINGAIR_15MPH" + + return r + + def areStandardOpaqueLayers(lc=None) -> bool: """Validates if every material in a layered construction is standard/opaque. @@ -715,7 +775,7 @@ def resetUo(lc=None, film=None, index=None, uo=None, uniq=False) -> float: mt.setName(id) if not mt.setThermalResistance(r): - oslg.log(CN.WRN, "Failed to reset %s: RSi%.2f (%s)" % (id, r, mth)) + oslg.log(CN.DBG, "Failed to reset %s: RSi%.2f (%s)" % (id, r, mth)) return 0.0 lc.setLayer(index, mt) @@ -745,12 +805,12 @@ def resetUo(lc=None, film=None, index=None, uo=None, uniq=False) -> float: mt.setName(id) if not mt.setThermalConductivity(k): - oslg.log(CN.WRN, "Failed to reset %s: K%.3f (%s)" % (id, k, mth)) + oslg.log(CN.DBG, "Failed to reset %s: K%.3f (%s)" % (id, k, mth)) return 0.0 if not mt.setThickness(d): d = int(d*1000) - oslg.log(CN.WRN, "Failed to reset %s: %dmm (%s)" % (id, d, mth)) + oslg.log(CN.DBG, "Failed to reset %s: %dmm (%s)" % (id, d, mth)) return 0.0 lc.setLayer(index, mt) @@ -836,8 +896,36 @@ def genConstruction(model=None, specs=dict()): a["compo"]["d" ] = d a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) + elif specs["type"] == "ceiling": + if specs["clad"] != "none": + mt = "concrete" + d = 0.015 + if specs["clad"] == "light": mt = "material" + if specs["clad"] == "medium": d = 0.100 + if specs["clad"] == "heavy": d = 0.200 + a["clad"]["mat"] = mats()[mt] + a["clad"]["d" ] = d + a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) + + mt = "mineral" + d = 0.100 + if specs["frame"] == "medium": mt = "polyiso" + if specs["frame"] == "heavy": mt = "cellulose" + if not u: mt = "material" + if not u: d = 0.015 + a["compo"]["mat"] = mats()[mt] + a["compo"]["d" ] = d + a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) + + if specs["finish"] != "none": + mt = "material" + d = 0.015 + a["finish"]["mat"] = mats()[mt] + a["finish"]["d" ] = d + a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) + elif specs["type"] == "partition": - if not specs["clad"] == "none": + if specs["clad"] != "none": mt = "drywall" d = 0.015 a["clad"]["mat"] = mats()[mt] @@ -855,7 +943,7 @@ def genConstruction(model=None, specs=dict()): a["compo"]["d" ] = d a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) - if not specs["finish"] == "none": + if specs["finish"] != "none": mt = "drywall" d = 0.015 a["finish"]["mat"] = mats()[mt] @@ -863,7 +951,7 @@ def genConstruction(model=None, specs=dict()): a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) elif specs["type"] == "wall": - if not specs["clad"] == "none": + if specs["clad"] != "none": mt = "material" d = 0.100 if specs["clad"] == "medium": mt = "brick" @@ -893,7 +981,7 @@ def genConstruction(model=None, specs=dict()): a["compo"]["d" ] = d a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) - if not specs["finish"] == "none": + if specs["finish"] != "none": mt = "concrete" d = 0.015 if specs["finish"] == "light": mt = "drywall" @@ -904,7 +992,7 @@ def genConstruction(model=None, specs=dict()): a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) elif specs["type"] == "roof": - if not specs["clad"] == "none": + if specs["clad"] != "none": mt = "concrete" d = 0.015 if specs["clad"] == "light": mt = "material" @@ -924,7 +1012,7 @@ def genConstruction(model=None, specs=dict()): a["compo"]["d" ] = d a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) - if not specs["finish"] == "none": + if specs["finish"] != "none": mt = "concrete" d = 0.015 if specs["finish"] == "light": mt = "drywall" @@ -935,7 +1023,7 @@ def genConstruction(model=None, specs=dict()): a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) elif specs["type"] == "floor": - if not specs["clad"] == "none": + if specs["clad"] != "none": mt = "material" d = 0.015 a["clad"]["mat"] = mats()[mt] @@ -952,7 +1040,7 @@ def genConstruction(model=None, specs=dict()): a["compo"]["d" ] = d a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) - if not specs["finish"] == "none": + if specs["finish"] != "none": mt = "concrete" d = 0.015 if specs["finish"] == "light": mt = "material" @@ -969,7 +1057,7 @@ def genConstruction(model=None, specs=dict()): a["clad"]["d" ] = d a["clad"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) - if not specs["frame"] == "none": + if specs["frame"] != "none": mt = "polyiso" d = 0.025 a["sheath"]["mat"] = mats()[mt] @@ -983,7 +1071,7 @@ def genConstruction(model=None, specs=dict()): a["compo"]["d" ] = d a["compo"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) - if not specs["finish"] == "none": + if specs["finish"] != "none": mt = "material" d = 0.015 a["finish"]["mat"] = mats()[mt] @@ -991,7 +1079,7 @@ def genConstruction(model=None, specs=dict()): a["finish"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) elif specs["type"] == "basement": - if not specs["clad"] == "none": + if specs["clad"] != "none": mt = "concrete" d = 0.100 if specs["clad"] == "light": mt = "material" @@ -1018,7 +1106,7 @@ def genConstruction(model=None, specs=dict()): a["sheath"]["d" ] = d a["sheath"]["id" ] = "OSut." + mt + ".%03d" % int(d * 1000) - if not specs["finish"] == "none": + if specs["finish"] != "none": mt = "mineral" d = 0.075 a["compo"]["mat"] = mats()[mt] @@ -2711,10 +2799,10 @@ def availabilitySchedule(model=None, avl=""): if not l.lowerLimitValue(): continue if not l.upperLimitValue(): continue if not l.numericType(): continue - if not int(l.lowerLimitValue().get()) == 0: continue - if not int(l.upperLimitValue().get()) == 1: continue - if not l.numericType().get().lower() == "discrete": continue - if not l.unitType().lower() == "availability": continue + if int(l.lowerLimitValue().get()) != 0: continue + if int(l.upperLimitValue().get()) != 1: continue + if l.numericType().get().lower() != "discrete": continue + if l.unitType().lower() != "availability": continue if ide != "hvac operation scheduletypelimits": continue limits = l diff --git a/tests/test_osut.py b/tests/test_osut.py index 1719699..5e9fd48 100644 --- a/tests/test_osut.py +++ b/tests/test_osut.py @@ -60,8 +60,8 @@ def test02_tuples(self): def test03_dictionaries(self): self.assertEqual(len(osut.mats()),9) - self.assertEqual(len(osut.film()),10) - self.assertEqual(len(osut.uo()),10) + self.assertEqual(len(osut.film()),11) + self.assertEqual(len(osut.uo()),11) self.assertTrue("concrete" in osut.mats()) self.assertTrue("skylight" in osut.film()) self.assertTrue("skylight" in osut.uo()) @@ -160,26 +160,6 @@ def test05_construction_generation(self): self.assertEqual(o.status(), 0) del model - # Alternative to (uninsulated) partition (more inputs, same outcome). - specs = dict(type="wall", clad="none", uo=None) - model = openstudio.model.Model() - c = osut.genConstruction(model, specs) - self.assertEqual(o.status(), 0) - self.assertFalse(o.logs()) - self.assertTrue(c) - self.assertTrue(isinstance(c, openstudio.model.Construction)) - self.assertEqual(c.nameString(), "OSut.CON.wall") - self.assertTrue(c.layers()) - self.assertEqual(len(c.layers()), 3) - self.assertEqual(c.layers()[0].nameString(), "OSut.drywall.015") - self.assertEqual(c.layers()[1].nameString(), "OSut.material.015") - self.assertEqual(c.layers()[2].nameString(), "OSut.drywall.015") - self.assertTrue("uo" in specs) - self.assertEqual(specs["uo"], None) - self.assertFalse(o.logs()) - self.assertEqual(o.status(), 0) - del model - # Insulated partition variant. specs = dict(type="partition", uo=0.214) model = openstudio.model.Model() @@ -192,7 +172,7 @@ def test05_construction_generation(self): self.assertTrue(c.layers()) self.assertEqual(len(c.layers()), 3) self.assertEqual(c.layers()[0].nameString(), "OSut.drywall.015") - self.assertEqual(c.layers()[1].nameString(), "OSut:K0.023:100") + self.assertEqual(c.layers()[1].nameString(), "OSut:K0.024:100") self.assertEqual(c.layers()[2].nameString(), "OSut.drywall.015") self.assertTrue("uo" in specs) self.assertAlmostEqual(specs["uo"], 0.214, places=2) @@ -906,13 +886,6 @@ def test08_holds_constructions(self): model = model.get() mdl = openstudio.model.Model() - # cl1 = openstudio.model.DefaultConstructionSet - # cl2 = openstudio.model.LayeredConstruction - # cl2 = openstudio.model.Construction - # id1 = cl1.__name__ - # id2 = cl2.__name__ - # id3 = cl3.__name__ - t1 = "RoofCeiling" t2 = "Wall" t3 = "Floor" @@ -1127,95 +1100,7 @@ def test10_glazing_airfilms(self): del model - def test11_rsi(self): - o = osut.oslg - self.assertEqual(o.status(), 0) - self.assertEqual(o.reset(DBG), DBG) - self.assertEqual(o.level(), DBG) - - version = int("".join(openstudio.openStudioVersion().split("."))) - translator = openstudio.osversion.VersionTranslator() - - path = openstudio.path("./tests/files/osms/out/seb2.osm") - model = translator.loadModel(path) - self.assertTrue(model) - model = model.get() - - m0 = "osut.rsi" - m1 = "'lc' str? expecting LayeredConstruction (%s)" % m0 - m2 = "'lc' NoneType? expecting LayeredConstruction (%s)" % m0 - m3 = "Negative 'film' (%s)" % m0 - m4 = "'film' NoneType? expecting float (%s)" % m0 - m5 = "Negative 'temp K' (%s)" % m0 - m6 = "'temp K' NoneType? expecting float (%s)" % m0 - - for s in model.getSurfaces(): - if not s.isPartOfEnvelope(): continue - - lc = s.construction() - self.assertTrue(lc) - lc = lc.get().to_LayeredConstruction() - self.assertTrue(lc) - lc = lc.get() - - if s.isGroundSurface(): # 4x slabs on grade in SEB model - self.assertAlmostEqual(s.filmResistance(), 0.160, places=3) - self.assertAlmostEqual(osut.rsi(lc, s.filmResistance()), 0.448, places=3) - self.assertEqual(o.status(), 0) - else: - if s.surfaceType() == "Wall": - self.assertAlmostEqual(s.filmResistance(), 0.150, places=3) - self.assertAlmostEqual(osut.rsi(lc, s.filmResistance()), 2.616, places=3) - self.assertEqual(o.status(), 0) - else: # RoofCeiling - self.assertAlmostEqual(s.filmResistance(), 0.136, places=3) - self.assertAlmostEqual(osut.rsi(lc, s.filmResistance()), 5.631, places=3) - self.assertEqual(o.status(), 0) - - # Stress tests. - self.assertAlmostEqual(osut.rsi("", 0.150), 0, places=2) - self.assertTrue(o.is_debug()) - self.assertEqual(len(o.logs()), 1) - self.assertEqual(o.logs()[0]["message"], m1) - self.assertEqual(o.clean(), DBG) - - self.assertAlmostEqual(osut.rsi(None, 0.150), 0, places=2) - self.assertTrue(o.is_debug()) - self.assertEqual(len(o.logs()), 1) - self.assertEqual(o.logs()[0]["message"], m2) - self.assertEqual(o.clean(), DBG) - - lc = model.getLayeredConstructionByName("SLAB-ON-GRADE-FLOOR") - self.assertTrue(lc) - lc = lc.get() - - self.assertAlmostEqual(osut.rsi(lc, -1), 0, places=0) - self.assertTrue(o.is_error()) - self.assertEqual(len(o.logs()), 1) - self.assertEqual(o.logs()[0]["message"], m3) - self.assertEqual(o.clean(), DBG) - - self.assertAlmostEqual(osut.rsi(lc, None), 0, places=0) - self.assertTrue(o.is_debug()) - self.assertEqual(len(o.logs()), 1) - self.assertEqual(o.logs()[0]["message"], m4) - self.assertEqual(o.clean(), DBG) - - self.assertAlmostEqual(osut.rsi(lc, 0.150, -300), 0, places=0) - self.assertTrue(o.is_error()) - self.assertEqual(len(o.logs()), 1) - self.assertEqual(o.logs()[0]["message"], m5) - self.assertEqual(o.clean(), DBG) - - self.assertAlmostEqual(osut.rsi(lc, 0.150, None), 0, places=0) - self.assertTrue(o.is_debug()) - self.assertEqual(len(o.logs()), 1) - self.assertEqual(o.logs()[0]["message"], m6) - self.assertEqual(o.clean(), DBG) - - del model - - def test12_insulating_layer(self): + def test11_insulating_layer(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1302,7 +1187,7 @@ def test12_insulating_layer(self): del model - def test13_spandrels(self): + def test12_spandrels(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1362,7 +1247,7 @@ def test13_spandrels(self): del model - def test14_schedule_ruleset_minmax(self): + def test13_schedule_ruleset_minmax(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1443,7 +1328,7 @@ def test14_schedule_ruleset_minmax(self): del model - def test15_schedule_constant_minmax(self): + def test14_schedule_constant_minmax(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1524,7 +1409,7 @@ def test15_schedule_constant_minmax(self): del model - def test16_schedule_compact_minmax(self): + def test15_schedule_compact_minmax(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1603,7 +1488,7 @@ def test16_schedule_compact_minmax(self): del model - def test17_minmax_heatcool_setpoints(self): + def test16_minmax_heatcool_setpoints(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1791,7 +1676,7 @@ def test17_minmax_heatcool_setpoints(self): del model - def test18_hvac_airloops(self): + def test17_hvac_airloops(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1836,7 +1721,7 @@ def test18_hvac_airloops(self): del model - def test19_vestibules(self): + def test18_vestibules(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -1906,7 +1791,7 @@ def test19_vestibules(self): del model - def test20_setpoints_plenums_attics(self): + def test19_setpoints_plenums_attics(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -2139,7 +2024,7 @@ def test20_setpoints_plenums_attics(self): # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # # Consider adding LargeOffice model to test SDK's "isPlenum" ... @todo - def test21_availability_schedules(self): + def test20_availability_schedules(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -2353,7 +2238,7 @@ def test21_availability_schedules(self): del model - def test22_model_transformation(self): + def test21_model_transformation(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -2682,7 +2567,7 @@ def test22_model_transformation(self): # if o.logs(): print(mod1.logs()) self.assertEqual(o.status(), 0) - def test23_fits_overlaps(self): + def test22_fits_overlaps(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -3246,7 +3131,7 @@ def test23_fits_overlaps(self): del model self.assertEqual(o.clean(), DBG) - def test24_triangulation(self): + def test23_triangulation(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -3288,7 +3173,7 @@ def test24_triangulation(self): # [ 0, 10, 0] # [20, 0, 0] - def test25_segments_triads_orientation(self): + def test24_segments_triads_orientation(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -3641,7 +3526,7 @@ def test25_segments_triads_orientation(self): self.assertEqual(len(matches), 1) - def test26_ulc_blc(self): + def test25_ulc_blc(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -3814,7 +3699,7 @@ def test26_ulc_blc(self): # [70, 45, 0] # [ 0, 45, 0] - def test27_polygon_attributes(self): + def test26_polygon_attributes(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(INF), INF) @@ -4030,7 +3915,7 @@ def test27_polygon_attributes(self): self.assertFalse(osut.isClockwise(v)) self.assertEqual(o.status(), 0) - def test28_subsurface_insertions(self): + def test27_subsurface_insertions(self): # Examples of how to harness OpenStudio's Boost geometry methods to # safely insert subsurfaces along rotated/tilted/slanted base surfaces. # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- # @@ -4375,7 +4260,7 @@ def test28_subsurface_insertions(self): del model self.assertEqual(o.clean(), DBG) - def test29_surface_width_height(self): + def test28_surface_width_height(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -4464,7 +4349,7 @@ def test29_surface_width_height(self): del model self.assertEqual(o.status(), 0) - def test30_wwr_insertions(self): + def test29_wwr_insertions(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -4678,7 +4563,7 @@ def test30_wwr_insertions(self): del model self.assertEqual(o.status(), 0) - def test31_convexity(self): + def test30_convexity(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(INF), INF) @@ -4864,7 +4749,7 @@ def test31_convexity(self): del model self.assertEqual(o.status(), 0) - def test32_outdoor_roofs(self): + def test31_outdoor_roofs(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(INF), INF) @@ -4957,7 +4842,7 @@ def test32_outdoor_roofs(self): del model self.assertEqual(o.status(), 0) - def test33_leader_line_anchors_inserts(self): + def test32_leader_line_anchors_inserts(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -5096,7 +4981,7 @@ def test33_leader_line_anchors_inserts(self): # [ 0, 14, 0] ... vs [20, 2, 20] # [ 0, 0, 0] ... vs [20, 16, 20] - def test34_generated_skylight_wells(self): + def test33_generated_skylight_wells(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -5255,9 +5140,10 @@ def test34_generated_skylight_wells(self): self.assertAlmostEqual(round(ratio, 2), srr) # Reset attic default construction set for insulated interzone walls. + flm = osut.filmResistances("partition") opts = dict(type="partition", uo=0.3) construction = osut.genConstruction(model, opts) - self.assertAlmostEqual(osut.rsi(construction, 0.150), 1/0.3, places=2) + self.assertAlmostEqual(osut.rsi(construction, flm), 1/0.3, places=2) self.assertTrue(ia_set.setWallConstruction(construction)) if o.logs(): print(o.logs()) @@ -5594,7 +5480,7 @@ def test34_generated_skylight_wells(self): self.assertEqual(o.status(), 0) del model - def test35_facet_retrieval(self): + def test34_facet_retrieval(self): o = osut.oslg self.assertEqual(o.status(), 0) self.assertEqual(o.reset(DBG), DBG) @@ -5667,6 +5553,396 @@ def test35_facet_retrieval(self): del model + def test35_rsi(self): + o = osut.oslg + self.assertEqual(o.status(), 0) + self.assertEqual(o.reset(DBG), DBG) + self.assertEqual(o.level(), DBG) + + version = int("".join(openstudio.openStudioVersion().split("."))) + translator = openstudio.osversion.VersionTranslator() + + # 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 + + # 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) + + # Surface type identifiers to fetch filmResistance values. + fts = dict() + 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 + + for i in list(openstudio.model.FilmResistanceType.getValues()): + t1 = openstudio.model.FilmResistanceType(i) + t2 = openstudio.model.FilmResistanceType(list(fts.keys())[i]) + r = openstudio.model.PlanarSurface.filmResistance(t1) + self.assertEqual(t1, t2) + self.assertAlmostEqual(r, list(fts.values())[i]) + + if i > 4: continue + # 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.0 + rsi = openstudio.model.PlanarSurface.stillAirFilmResistance(rad) + # per = 100 * (r - rsi) / r + # print(i, deg, r, rsi, per) + # 0: 0: 0.10743 0.10604 1.29 + # 1: 45: 0.10919 0.10944 -0.23 + # 2: 90: 0.11975 0.11965 0.09 + # 3: 135: 0.13384 0.13665 -2.10 + # 4: 180: 0.16202 0.16045 0.97 + if deg < 45 or deg > 90: continue + + # The method is used for (opaque) Surfaces. The correlation/regression + # isn't perfect, yet appears fairly reliable for intermediate angles + # between ~0° and 90°. + self.assertAlmostEqual(rsi, r, places=3) + + # 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. + self.assertEqual(o.clean(), DBG) + + path = openstudio.path("./tests/files/osms/out/office_attic.osm") + model = translator.loadModel(path) + self.assertTrue(model) + model = model.get() + + attic = model.getSpaceByName("Attic") + self.assertTrue(attic) + attic = attic.get() + self.assertTrue(osut.isUnconditioned(attic)) + + # Test attic (insulated) floors, then their adjacent ceilings. + for surface in model.getSurfaces(): + if surface.surfaceType().lower() != "floor": continue + if surface.outsideBoundaryCondition().lower() != "surface": continue + + space = surface.space() + self.assertTrue(space) + space = space.get() + self.assertEqual(space, attic) + + id = surface.nameString() + self.assertIn("attic_floor", id.lower()) + + # A surface's 'tilt' points outward (from its parent space), e.g. a + # horizontal attic floor faces downward, or 180°. + tilt = surface.tilt() + self.assertAlmostEqual(tilt, math.pi, places=4) + + r1 = openstudio.model.PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance() + r3 = osut.filmResistances("ceiling") + r4 = osut.filmResistances("ceiling", tilt) + self.assertAlmostEqual(r1, 0.321, places=3) + self.assertAlmostEqual(r2, 0.321, places=3) + self.assertAlmostEqual(r3, 0.266, places=3) + self.assertAlmostEqual(r4, 0.266, places=3) + + # Test adjacent ceilings. + ceiling = surface.adjacentSurface() + self.assertTrue(ceiling) + ceiling = ceiling.get() + + nom = ceiling.nameString() + self.assertIn("_ceiling", nom) + + # A horizontal ceiling faces upward, or 0°. + tilt = ceiling.tilt() + self.assertAlmostEqual(tilt, 0, places=4) + + r1 = openstudio.model.PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = ceiling.filmResistance() + r3 = osut.filmResistances("ceiling") + r4 = osut.filmResistances("ceiling", tilt) + self.assertAlmostEqual(r1, 0.212, places=3) # not 0.321! + self.assertAlmostEqual(r2, 0.212, places=3) # not 0.321! + self.assertAlmostEqual(r3, 0.266, places=3) # same as floor above + self.assertAlmostEqual(r4, 0.266, places=3) # same as floor above + + # OS-reported film resistances: 0.212 vs 0.321 - which one? + # + # ---------------------------------------------------------------------- + # 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). + + # Skylight well walls? + for surface in attic.surfaces(): + if surface.surfaceType().lower() != "wall": continue + + # Skylight well walls are vertical. + tilt = surface.tilt() + self.assertAlmostEqual(tilt, math.pi/2, places=4) + self.assertEqual(surface.outsideBoundaryCondition().lower(), "surface") + + r1 = openstudio.model.PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance() + self.assertAlmostEqual(r1, 0.239, places=2) + self.assertAlmostEqual(r2, 0.239, places=2) + + # Adjacent skylight well walls? + adjacent = surface.adjacentSurface() + self.assertTrue(adjacent) + adjacent = adjacent.get() + + r1 = openstudio.model.PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = adjacent.filmResistance() + r3 = osut.filmResistances("partition") + r4 = osut.filmResistances("partition", tilt) + self.assertAlmostEqual(r1, 0.239, places=3) + self.assertAlmostEqual(r2, 0.239, places=3) + self.assertAlmostEqual(r3, 0.239, places=3) + self.assertAlmostEqual(r4, 0.239, places=3) + + # Different from interzone walls in CONDITIONED, occupied spaces? + for space in model.getSpaces(): + if space != attic: continue + + for surface in space.surfaces(): + if surface.surfaceType().lower() != "wall": continue + if surface.outsideBoundaryCondition().lower() != "surface": continue + + tilt = surface.tilt() + self.assertAlmostEqual(tilt, math.pi/2, places=4) + + r1 = openstudio.model.PlanarSurface.stillAirFilmResistance(tilt) * 2 + r2 = surface.filmResistance() + r3 = osut.filmResistances("partition") + r4 = osut.filmResistances("partition", tilt) + self.assertAlmostEqual(r1, 0.239, places=3) + self.assertAlmostEqual(r2, 0.239, places=3) + self.assertAlmostEqual(r3, 0.239, places=3) + self.assertAlmostEqual(r4, 0.239, places=3) + + # 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. + path = openstudio.path("./tests/files/osms/out/seb2.osm") + model = translator.loadModel(path) + self.assertTrue(model) + model = model.get() + + m0 = "osut.rsi" + m1 = "'lc' str? expecting LayeredConstruction (%s)" % m0 + m2 = "'lc' NoneType? expecting LayeredConstruction (%s)" % m0 + m3 = "Negative 'film' (%s)" % m0 + m4 = "'film' NoneType? expecting float (%s)" % m0 + m5 = "Negative 'temp K' (%s)" % m0 + m6 = "'temp K' NoneType? expecting float (%s)" % m0 + + for s in model.getSurfaces(): + if not s.isPartOfEnvelope(): continue + + lc = s.construction() + self.assertTrue(lc) + lc = lc.get().to_LayeredConstruction() + self.assertTrue(lc) + 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 = osut.filmResistances("slab") + r3 = osut.filmResistances("slab", s.tilt()) + self.assertAlmostEqual(r1, 0.160, places=3) + self.assertAlmostEqual(r2, 0.162, places=3) + self.assertAlmostEqual(r3, 0.160, places=3) + self.assertAlmostEqual(osut.rsi(lc, r1), 0.448, places=3) + self.assertAlmostEqual(osut.rsi(lc, r2), 0.450, places=3) + self.assertAlmostEqual(osut.rsi(lc, r3), 0.448, places=3) + elif s.surfaceType().lower() == "wall": + r2 = osut.filmResistances("wall") + r3 = osut.filmResistances("wall", s.tilt()) + self.assertAlmostEqual(r1, 0.150, places=3) + self.assertAlmostEqual(r2, 0.150, places=3) + self.assertAlmostEqual(r3, 0.150, places=3) + self.assertAlmostEqual(osut.rsi(lc, r1), 2.616, places=3) + self.assertAlmostEqual(osut.rsi(lc, r2), 2.617, places=3) + self.assertAlmostEqual(osut.rsi(lc, r3), 2.616, places=3) + else: # RoofCeiling + self.assertEqual(s.surfaceType().lower(), "roofceiling") + r2 = osut.filmResistances("roof") + r3 = osut.filmResistances("roof", s.tilt()) + self.assertAlmostEqual(r1, 0.136, places=3) + self.assertAlmostEqual(r2, 0.135, places=3) + self.assertAlmostEqual(r3, 0.136, places=3) + self.assertAlmostEqual(osut.rsi(lc, r1), 5.631, places=3) + self.assertAlmostEqual(osut.rsi(lc, r2), 5.630, places=3) + self.assertAlmostEqual(osut.rsi(lc, r3), 5.631, places=3) + else: + self.assertEqual(s.outsideBoundaryCondition().lower(), "surface") + + if s.surfaceType().lower() == "wall": + r2 = osut.filmResistances("wall") + r3 = osut.filmResistances("wall", s.tilt()) + self.assertAlmostEqual(r1, 0.239, places=3) + self.assertAlmostEqual(r2, 0.239, places=3) + self.assertAlmostEqual(r3, 0.239, places=3) + self.assertAlmostEqual(osut.rsi(lc, r1), 0.680, places=3) + self.assertAlmostEqual(osut.rsi(lc, r2), 0.680, places=3) + self.assertAlmostEqual(osut.rsi(lc, r3), 0.680, places=3) + elif s.surfaceType().lower() == "roofceiling": + r2 = osut.filmResistances("ceiling") + r3 = osut.filmResistances("ceiling", s.tilt()) + self.assertAlmostEqual(r1, 0.212, places=3) # attic ceiling + self.assertAlmostEqual(r2, 0.266, places=3) + self.assertAlmostEqual(r3, 0.266, places=3) + self.assertAlmostEqual(osut.rsi(lc, r1), 0.331, places=3) + self.assertAlmostEqual(osut.rsi(lc, r2), 0.386, places=3) + self.assertAlmostEqual(osut.rsi(lc, r3), 0.386, places=3) + else: + self.assertEqual(s.surfaceType().lower(), "floor") + r2 = osut.filmResistances("ceiling") + r3 = osut.filmResistances("ceiling", s.tilt()) + self.assertAlmostEqual(r1, 0.321, places=3) # attic floor + self.assertAlmostEqual(r2, 0.266, places=3) + self.assertAlmostEqual(r3, 0.266, places=3) + self.assertAlmostEqual(osut.rsi(lc, r1), 0.440, places=3) + self.assertAlmostEqual(osut.rsi(lc, r2), 0.386, places=3) + self.assertAlmostEqual(osut.rsi(lc, r3), 0.386, places=3) + + # Stress tests. + self.assertEqual(o.status(), 0) + self.assertAlmostEqual(osut.rsi("", 0.150), 0, places=2) + self.assertTrue(o.is_debug()) + self.assertEqual(len(o.logs()), 1) + self.assertEqual(o.logs()[0]["message"], m1) + self.assertEqual(o.clean(), DBG) + + self.assertAlmostEqual(osut.rsi(None, 0.150), 0, places=2) + self.assertTrue(o.is_debug()) + self.assertEqual(len(o.logs()), 1) + self.assertEqual(o.logs()[0]["message"], m2) + self.assertEqual(o.clean(), DBG) + + lc = model.getLayeredConstructionByName("SLAB-ON-GRADE-FLOOR") + self.assertTrue(lc) + lc = lc.get() + + self.assertAlmostEqual(osut.rsi(lc, -1), 0, places=0) + self.assertTrue(o.is_error()) + self.assertEqual(len(o.logs()), 1) + self.assertEqual(o.logs()[0]["message"], m3) + self.assertEqual(o.clean(), DBG) + + self.assertAlmostEqual(osut.rsi(lc, None), 0, places=0) + self.assertTrue(o.is_debug()) + self.assertEqual(len(o.logs()), 1) + self.assertEqual(o.logs()[0]["message"], m4) + self.assertEqual(o.clean(), DBG) + + self.assertAlmostEqual(osut.rsi(lc, 0.150, -300), 0, places=0) + self.assertTrue(o.is_error()) + self.assertEqual(len(o.logs()), 1) + self.assertEqual(o.logs()[0]["message"], m5) + self.assertEqual(o.clean(), DBG) + + self.assertAlmostEqual(osut.rsi(lc, 0.150, None), 0, places=0) + self.assertTrue(o.is_debug()) + self.assertEqual(len(o.logs()), 1) + self.assertEqual(o.logs()[0]["message"], m6) + self.assertEqual(o.clean(), DBG) + + del model + def test36_slab_generation(self): o = osut.oslg self.assertEqual(o.status(), 0)