@@ -51,7 +51,7 @@ def test01_osm_instantiation(self):
5151 model = openstudio .model .Model ()
5252 self .assertTrue (isinstance (model , openstudio .model .Model ))
5353 del model
54-
54+
5555 def test02_tuples (self ):
5656 self .assertEqual (len (osut .sidz ()), 6 )
5757 self .assertEqual (len (osut .mass ()), 4 )
@@ -5010,6 +5010,180 @@ def test34_generated_skylight_wells(self):
50105010
50115011 model .save ("./tests/files/osms/out/office_attic.osm" , True )
50125012
5013+ # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
5014+ # Side test/comment: Why is it necessary to have 'addSkylights' return
5015+ # gross roof area (see 'rm2' above)?
5016+ #
5017+ # First, retrieving (newly-added) core roofs (i.e. skylight base
5018+ # surfaces).
5019+ rfs1 = osut .facets (core , "Outdoors" , "RoofCeiling" )
5020+ tot1 = sum ([sk .grossArea () for sk in rfs1 ])
5021+ net = sum ([sk .netArea () for sk in rfs1 ])
5022+ self .assertEqual (len (rfs1 ), 4 )
5023+ self .assertAlmostEqual (tot1 , 9.06 , places = 2 ) # 4x 2.265 m2
5024+ self .assertAlmostEqual (tot1 - net , sky_area1 , places = 2 )
5025+
5026+ # In absence of skylight wells (more importantly, in absence of leader
5027+ # lines anchoring skylight base surfaces), OSut's 'roofs' &
5028+ # 'grossRoofArea' report not only on newly-added base surfaces (or
5029+ # their areas), but also overalpping areas of attic roofs above.
5030+ # Unfortunately, these become unreliable with newly-added skylight wells.
5031+ rfs2 = osut .roofs (core )
5032+ tot2 = sum ([sk .grossArea () for sk in rfs2 ])
5033+ self .assertAlmostEqual (tot2 , tot1 , places = 2 )
5034+ self .assertAlmostEqual (tot2 , osut .grossRoofArea (core ), places = 2 )
5035+
5036+ # Fortunately, the addition of leader lines does not affect how
5037+ # OpenStudio reports surface areas.
5038+ rfs3 = osut .facets (attic , "Outdoors" , "RoofCeiling" )
5039+ tot3 = sum ([sk .grossArea () for sk in rfs3 ])
5040+ self .assertAlmostEqual (tot3 + tot2 , total2 , places = 2 ) # 598.76
5041+
5042+ # However, as discussed elsewhere (see 'addSkylights' doctring and
5043+ # inline comments), these otherwise valid areas are often overestimated
5044+ # for SRR% calculations (e.g. when overhangs and soffits are explicitely
5045+ # modelled). It is for this reason 'addSkylights' reports gross roof
5046+ # area BEFORE adding skylight wells. For higher-level applications
5047+ # relying on 'addSkylights' (e.g. an OpenStudio measure), it is better
5048+ # to store returned gross roof areas for subsequent reporting purposes.
5049+
5050+ # Deeper dive: Why are OSut's 'roofs' and 'grossRoofArea' unreliable
5051+ # with leader lines? Both rely on OSut's 'overlapping', itself relying
5052+ # on OpenStudio's 'join' and 'intersect': if neither are successful in
5053+ # joining (or intersecting) 2x polygons (e.g. attic roof vs cast core
5054+ # ceiling), there can be no identifiable overlap. In such cases, both
5055+ # 'roofs' and 'grossRoofArea' ignore overlapping attic roofs. A demo:
5056+ roof_north = model .getSurfaceByName ("Attic_roof_north" )
5057+ core_ceiling = model .getSurfaceByName ("Core_ZN_ceiling" )
5058+ self .assertTrue (roof_north )
5059+ self .assertTrue (core_ceiling )
5060+ roof_north = roof_north .get ()
5061+ core_ceiling = core_ceiling .get ()
5062+
5063+ t = openstudio .Transformation .alignFace (roof_north .vertices ())
5064+ up = openstudio .Point3d (0 ,0 ,1 ) - openstudio .Point3d (0 ,0 ,0 )
5065+
5066+ a_roof_north = t .inverse () * roof_north .vertices ()
5067+ a_core_ceiling = t .inverse () * core_ceiling .vertices ()
5068+ c_core_ceiling = osut .cast (a_core_ceiling , a_roof_north , up )
5069+
5070+ north_m2 = openstudio .getArea (a_roof_north )
5071+ ceiling_m2 = openstudio .getArea (c_core_ceiling )
5072+ self .assertTrue (north_m2 )
5073+ self .assertTrue (ceiling_m2 )
5074+ self .assertAlmostEqual (north_m2 .get (), 192.98 , places = 2 )
5075+ self .assertAlmostEqual (ceiling_m2 .get (), 133.81 , places = 2 )
5076+
5077+ # So far so good. Ensure clockwise winding.
5078+ a_roof_north = list (a_roof_north )
5079+ c_core_ceiling = list (c_core_ceiling )
5080+ a_roof_north .reverse ()
5081+ c_core_ceiling .reverse ()
5082+ self .assertFalse (openstudio .join (a_roof_north , c_core_ceiling , TOL2 ))
5083+ self .assertFalse (openstudio .intersect (a_roof_north , c_core_ceiling , TOL ))
5084+
5085+ # A future revision of OSut's 'roofs' and 'grossRoofArea' would require:
5086+ # - a new method identifying leader lines amongst surface vertices
5087+ # - a new method identifying surface cutouts amongst surface vertices
5088+ # - a method to prune both leader lines and cutouts from surface vertices
5089+ # - have 'roofs' & 'grossRoofArea' rely on the remaining outer vertices
5090+ # ... @todo?
5091+ self .assertEqual (o .status (), 0 )
5092+ del model
5093+
5094+ # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
5095+ # CASE 2:
5096+ path = openstudio .path ("./tests/files/osms/in/smalloffice.osm" )
5097+ model = translator .loadModel (path )
5098+ self .assertTrue (model )
5099+ model = model .get ()
5100+
5101+ core = model .getSpaceByName ("Core_ZN" )
5102+ attic = model .getSpaceByName ("Attic" )
5103+ self .assertTrue (core )
5104+ self .assertTrue (attic )
5105+ core = core .get ()
5106+ attic = attic .get ()
5107+
5108+ # Tag attic as an INDIRECTLY-CONDITIONED space.
5109+ key = "indirectlyconditioned"
5110+ val = core .nameString ()
5111+ self .assertTrue (attic .additionalProperties ().setFeature (key , val ))
5112+ self .assertFalse (osut .arePlenums (attic ))
5113+ self .assertFalse (osut .isUnconditioned (attic ))
5114+ self .assertAlmostEqual (osut .setpoints (attic )["heating" ], 21.11 , places = 2 )
5115+ self .assertAlmostEqual (osut .setpoints (attic )["cooling" ], 23.89 , places = 2 )
5116+
5117+ # Here, GRA includes ALL plenum roof surfaces (not just vertically-cast
5118+ # roof areas onto the core ceiling). More roof surfaces == greater
5119+ # skylight areas to meet the SRR% of 5%.
5120+ gra_plenum = osut .grossRoofArea (core )
5121+ self .assertAlmostEqual (gra_plenum , total1 , places = 2 )
5122+
5123+ rm2 = osut .addSkyLights (core , dict (srr = srr ))
5124+ if o .logs (): print (o .logs ())
5125+ self .assertAlmostEqual (rm2 , total1 , places = 2 )
5126+
5127+ # The total skylight area is greater than in CASE 1. Nonetheless, the
5128+ # method is able to meet the requested SRR 5%. This may not be
5129+ # achievable in other circumstances, given the constrained roof/core
5130+ # overlap. Although a plenum vastly larger than the room(s) it serves is
5131+ # rare, it remains certainly problematic for the application of the
5132+ # Canadian NECB reference building skylight requirements.
5133+ core_skies = osut .facets (core , "Outdoors" , "Skylight" )
5134+ sky_area2 = sum ([sk .grossArea () for sk in core_skies ])
5135+ self .assertAlmostEqual (sky_area2 , 29.94 , places = 2 )
5136+ ratio2 = sky_area2 / rm2
5137+ self .assertAlmostEqual (ratio2 , srr , places = 2 )
5138+
5139+ model .save ("./tests/files/osms/out/office_plenum.osm" , True )
5140+
5141+ self .assertEqual (o .status (), 0 )
5142+ del model
5143+
5144+ # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
5145+ # CASE 2b:
5146+ path = openstudio .path ("./tests/files/osms/in/smalloffice.osm" )
5147+ model = translator .loadModel (path )
5148+ self .assertTrue (model )
5149+ model = model .get ()
5150+
5151+ core = model .getSpaceByName ("Core_ZN" )
5152+ attic = model .getSpaceByName ("Attic" )
5153+ self .assertTrue (core )
5154+ self .assertTrue (attic )
5155+ core = core .get ()
5156+ attic = attic .get ()
5157+
5158+ # Again, tagging attic as an INDIRECTLY-CONDITIONED space.
5159+ key = "indirectlyconditioned"
5160+ val = core .nameString ()
5161+ self .assertTrue (attic .additionalProperties ().setFeature (key , val ))
5162+ self .assertFalse (osut .arePlenums (attic ))
5163+ self .assertFalse (osut .isUnconditioned (attic ))
5164+ self .assertAlmostEqual (osut .setpoints (attic )["heating" ], 21.11 , places = 2 )
5165+ self .assertAlmostEqual (osut .setpoints (attic )["cooling" ], 23.89 , places = 2 )
5166+
5167+ gra_plenum = osut .grossRoofArea (core )
5168+ self .assertAlmostEqual (gra_plenum , total1 , places = 2 )
5169+
5170+ # Conflicting argument case: Here, skylight wells must traverse plenums
5171+ # (in this context, "plenum" is an all encompassing keyword for any
5172+ # INDIRECTLY-CONDITIONED, unoccupied space). Yet by passing option
5173+ # "plenum: False", the method is instructed to skip "plenum" skylight
5174+ # wells altogether.
5175+ rm2 = osut .addSkyLights (core , dict (srr = srr , plenum = False ))
5176+ self .assertTrue (o .is_warn ())
5177+ self .assertEqual (len (o .logs ()), 1 )
5178+ msg = o .logs ()[0 ]["message" ]
5179+ self .assertTrue ("Empty 'subsets (3)' (osut.addSkyLights)" in msg )
5180+ self .assertAlmostEqual (rm2 , total1 , places = 2 )
5181+
5182+ core_skies = osut .facets (core , "Outdoors" , "Skylight" )
5183+ sky_area2 = sum ([sk .grossArea () for sk in core_skies ])
5184+ self .assertAlmostEqual (sky_area2 , 0.00 , places = 2 )
5185+ self .assertEqual (o .clean (), DBG )
5186+
50135187 self .assertEqual (o .status (), 0 )
50145188 del model
50155189
0 commit comments