2828# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929
3030import re
31+ import math
32+ import collections
3133import openstudio
3234from oslg import oslg
3335from dataclasses import dataclass
3436
3537@dataclass (frozen = True )
3638class _CN :
37- DBG = oslg .CN .DEBUG
38- INF = oslg .CN .INFO
39- WRN = oslg .CN .WARN
40- ERR = oslg .CN .ERROR
41- FTL = oslg .CN .FATAL
42- NS = "nameString"
39+ DBG = oslg .CN .DEBUG
40+ INF = oslg .CN .INFO
41+ WRN = oslg .CN .WARN
42+ ERR = oslg .CN .ERROR
43+ FTL = oslg .CN .FATAL
44+ NS = "nameString"
45+ TOL = 0.01 # default distance tolerance (m)
46+ TOL2 = TOL * TOL # default area tolerance (m2)
4347CN = _CN ()
4448
4549# General surface orientations (see 'facets' method).
@@ -207,15 +211,15 @@ def uo() -> dict:
207211
208212
209213def are_standardOpaqueLayers (lc = None ) -> bool :
210- """
211- Validates if every material in a layered construction is standard & opaque.
214+ """Validates if every material in a layered construction is standard & opaque.
212215
213216 Args:
214- lc:
217+ lc (openstudio.model.LayeredConstruction) :
215218 an OpenStudio layered construction
216219
217220 Returns:
218- Whether all layers are valid. False if invalid inputs (see logs).
221+ True: If all layers are valid (standard & opaque).
222+ False: If invalid inputs (see logs).
219223
220224 """
221225 mth = "osut.are_standardOpaqueLayers"
@@ -236,15 +240,15 @@ def are_standardOpaqueLayers(lc=None) -> bool:
236240
237241
238242def thickness (lc = None ) -> float :
239- """
240- Returns total (standard opaque) layered construction thickness (m).
243+ """Returns total (standard opaque) layered construction thickness (m).
241244
242245 Args:
243- lc:
246+ lc (openstudio.model.LayeredConstruction) :
244247 an OpenStudio layered construction
245248
246249 Returns:
247- Construction thickness. 0.0 if invalid inputs (see logs).
250+ float: A standard opaque construction thickness.
251+ 0.0: If invalid inputs (see logs).
248252
249253 """
250254 mth = "osut.thickness"
@@ -269,22 +273,22 @@ def thickness(lc=None) -> float:
269273
270274
271275def rsi (lc = None , film = 0.0 , t = 0.0 ) -> float :
272- """
273- Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
274- includes air film resistances. It excludes insulating effects of shades,
275- screens, etc. in the case of fenestrated constructions. Adapted from BTAP's
276- 'Material' Module "get_conductance" (P. Lopez).
276+ """Returns a construction's 'standard calc' thermal resistance (m2•K/W),
277+ which includes air film resistances. It excludes insulating effects of
278+ shades, screens, etc. in the case of fenestrated constructions. Adapted
279+ from BTAP's 'Material' Module "get_conductance" (P. Lopez).
277280
278281 Args:
279- lc:
282+ lc (openstudio.model.LayeredConstruction) :
280283 an OpenStudio layered construction
281- film:
284+ film (float) :
282285 thermal resistance of surface air films (m2•K/W)
283- t:
286+ t (float) :
284287 gas temperature (°C) (optional)
285288
286289 Returns:
287- Layered construction's thermal resistance (0 if invalid input, see logs).
290+ float: A layered construction's thermal resistance.
291+ 0.0: If invalid input (see logs).
288292
289293 """
290294 mth = "osut.rsi"
@@ -344,23 +348,21 @@ def rsi(lc=None, film=0.0, t=0.0) -> float:
344348
345349
346350def insulatingLayer (lc = None ) -> dict :
347- """
348- Identifies a layered construction's (opaque) insulating layer. Returns an
349- insulating-layer dictionary:
350- "index": insulating layer index [0, n layers) within construction
351- "type" : layer material type ("standard" or "massless")
352- "r" : material thermal resistance in m2•K/W.
353- If unsuccessful, DEBUG errors are logged. Dictionary is voided as follows:
354- "index": None
355- "type" : None
356- "r" : 0.0
351+ """Identifies a layered construction's (opaque) insulating layer.
357352
358353 Args:
359- lc:
360- [openStudio.model.LayeredConstruction] a layered construction
354+ lc (openStudio.model.LayeredConstruction) :
355+ an OpenStudio layered construction
361356
362357 Returns:
363- Insulating layer dictionary.
358+ An insulating-layer dictionary:
359+ - "index" (int): construction's insulating layer index [0, n layers)
360+ - "type" (str): layer material type ("standard" or "massless")
361+ - "r" (float): material thermal resistance in m2•K/W.
362+ If unsuccessful, dictionary is voided as follows (see logs):
363+ "index": None
364+ "type": None
365+ "r": 0.0
364366
365367 """
366368 mth = "osut.insulatingLayer"
@@ -407,21 +409,21 @@ def insulatingLayer(lc=None) -> dict:
407409
408410
409411def genConstruction (model = None , specs = dict ()):
410- """
411- Generates an OpenStudio multilayered construction, + materials if needed.
412+ """Generates an OpenStudio multilayered construction, + materials if needed.
412413
413414 Args:
414415 specs:
415416 A dictionary holding multilayered construction parameters:
416- - "id": construction identifier
417- - "type": surface type - see OSut 'uo()'
418- - "uo": assembly clear-field Uo, in W/m2•K - see OSut 'uo()'
419- - "clad": exterior cladding - see OSut 'mass()'
420- - "frame": assembly framing - see OSut 'mass()'
421- - "finish": interior finish - see OSut 'mass()'
417+ - "id" (str) : construction identifier
418+ - "type" (str) : surface type - see OSut 'uo()'
419+ - "uo" (float) : assembly clear-field Uo, in W/m2•K - see OSut 'uo()'
420+ - "clad" (str) : exterior cladding - see OSut 'mass()'
421+ - "frame" (str) : assembly framing - see OSut 'mass()'
422+ - "finish" (str) : interior finish - see OSut 'mass()'
422423
423424 Returns:
424- Generated construction, or None if invalid inputs (see logs).
425+ openstudio.model.Construction: A generated construction.
426+ None: If invalid inputs (see logs).
425427
426428 """
427429 mth = "osut.genConstruction"
@@ -789,9 +791,8 @@ def genConstruction(model=None, specs=dict()):
789791 return c
790792
791793
792- def genShade (subs = openstudio .model .SubSurfaceVector ()) -> bool :
793- """
794- Generates solar shade(s) (e.g. roller, textile) for glazed OpenStudio
794+ def genShade (subs = []) -> bool :
795+ """Generates solar shade(s) (e.g. roller, textile) for glazed OpenStudio
795796 SubSurfaces (v321+), controlled to minimize overheating in cooling months
796797 (May to October in Northern Hemisphere), when outdoor dry bulb temperature
797798 is above 18°C and impinging solar radiation is above 100 W/m2.
@@ -801,7 +802,8 @@ def genShade(subs=openstudio.model.SubSurfaceVector()) -> bool:
801802 A list of sub surfaces.
802803
803804 Returns:
804- Whether successfully generated. False if invalid input (see logs).
805+ True: If successfully generated shade.
806+ False: if invalid input (see logs).
805807
806808 """
807809 # Filter OpenStudio warnings for ShadingControl:
@@ -890,3 +892,207 @@ def genShade(subs=openstudio.model.SubSurfaceVector()) -> bool:
890892 ctl .setSubSurfaces (subs )
891893
892894 return True
895+
896+
897+ def transforms (group = None ) -> dict :
898+ """"Returns OpenStudio site/space transformation & rotation angle.
899+
900+ Args:
901+ group:
902+ A site or space PlanarSurfaceGroup object.
903+
904+ Returns:
905+ A transformation + rotation dictionary:
906+ - t (openstudio.Transformation): site/space transformation.
907+ None: if invalid inputs (see logs).
908+ - r (float): Site/space rotation angle [0,2PI) radians.
909+ None: if invalid inputs (see logs).
910+
911+ """
912+ mth = "osut.transforms"
913+ res = dict (t = None , r = None )
914+ cl = openstudio .model .PlanarSurfaceGroup
915+
916+ if not hasattr (group , CN .NS ):
917+ return oslg .invalid ("group" , mth , 0 , CN .DBG , res )
918+
919+ id = group .nameString ()
920+ mdl = group .model ()
921+
922+ if isinstance (group , cl ):
923+ return oslg .mismatch (id , group , cl , mth , CN .DBG , res )
924+
925+ res ["t" ] = group .siteTransformation ()
926+ res ["r" ] = group .directionofRelativeNorth () + mdl .getBuilding ().northAxis ()
927+
928+ return res
929+
930+
931+ def trueNormal (s = None , r = 0 ):
932+ """Returns the site/true outward normal vector of a surface.
933+
934+ Args:
935+ s (OpenStudio::Model::PlanarSurface):
936+ An OpenStudio Planar Surface.
937+ r (float):
938+ a group/site rotation angle [0,2PI) radians
939+
940+ Returns:
941+ openstudio.Vector3d: A surface's true normal vector.
942+ None : If invalid input (see logs).
943+
944+ """
945+ mth = "osut.trueNormal"
946+ cl = openstudio .model .PlanarSurface
947+
948+ if not isinstance (s , cl ):
949+ return oslg .mismatch ("surface" , s , cl , mth )
950+
951+ try :
952+ r = float (r )
953+ except ValueError as e :
954+ return oslg .mismatch ("rotation" , r , float , mth )
955+
956+ r = float (- r ) * math .pi / 180.0
957+
958+ vx = s .outwardNormal ().x * math .cos (r ) - s .outwardNormal ().y * math .sin (r )
959+ vy = s .outwardNormal ().x * math .sin (r ) + s .outwardNormal ().y * math .cos (r )
960+ vz = s .outwardNormal ().z
961+
962+ return openstudio .Point3d (vx , vy , vz ) - openstudio .Point3d (0 , 0 , 0 )
963+
964+
965+ def scalar (v = None , m = 0 ) -> openstudio .Vector3d :
966+ """Returns scalar product of an OpenStudio Vector3d.
967+
968+ Args:
969+ v (OpenStudio::Vector3d):
970+ An OpenStudio vector.
971+ m (float):
972+ A scalar.
973+
974+ Returns:
975+ (openstudio.Vector3d) scaled points (see logs if (0,0,0)).
976+
977+ """
978+ mth = "osut.scalar"
979+ cl = openstudio .Vector3d
980+ v0 = openstudio .Vector3d ()
981+
982+ if not isinstance (v , cl ):
983+ return oslg .mismatch ("vector" , v , cl , mth , CN .DBG , v0 )
984+
985+ try :
986+ m = float (m )
987+ except ValueError as e :
988+ return oslg .mismatch ("scalar" , m , float , mth , CN .DBG , v0 )
989+
990+ v0 = openstudio .Vector3d (m * v .x (), m * v .y (), m * v .z ())
991+
992+ return v0
993+
994+
995+ def to_p3Dv (pts = None ) -> openstudio .Point3dVector :
996+ """Returns OpenStudio 3D points as an OpenStudio point vector, validating
997+ points in the process.
998+
999+ Args:
1000+ pts (list): OpenStudio 3D points.
1001+
1002+ Returns:
1003+ openstudio.Point3dVector: Vector of 3D points (see logs if empty).
1004+
1005+ """
1006+ mth = "osut.to_p3Dv"
1007+ cl = openstudio .Point3d
1008+ v = openstudio .Point3dVector ()
1009+
1010+ if isinstance (pts , cl ):
1011+ v .append (pts )
1012+ return v
1013+ elif isinstance (pts , openstudio .Point3dVector ):
1014+ return pts
1015+ elif isinstance (pts , openstudio .model .PlanarSurface ):
1016+ return pts .vertices ()
1017+
1018+ try :
1019+ pts = list (pts )
1020+ except ValueError as e :
1021+ return oslg .mismatch ("points" , pts , list , mth , CN .DBG , v )
1022+
1023+ for pt in pts :
1024+ if not isinstance (pt , cl ):
1025+ return oslg .mismatch ("point" , pt , cl , mth , CN .DBG , v )
1026+
1027+ for pt in pts :
1028+ v .append (openstudio .Point3d (pt .x (), pt .y (), pt .z ()))
1029+
1030+ return v
1031+
1032+
1033+ def is_same_vtx (s1 = None , s2 = None , indexed = True ) -> bool :
1034+ """Returns True if 2 sets of OpenStudio 3D points are nearly equal.
1035+
1036+ Args:
1037+ s1:
1038+ 1st set of OpenStudio 3D points
1039+ s2:
1040+ 2nd set of OpenStudio 3D points
1041+ indexed (bool):
1042+ whether to attempt to harmonize vertex sequence
1043+
1044+ Returns:
1045+ bool: Whether sets are nearly equal (within TOL).
1046+ False: If invalid input (see logs).
1047+
1048+ """
1049+ try :
1050+ s1 = list (s1 )
1051+ except ValueError as e :
1052+ return False
1053+
1054+ try :
1055+ s2 = list (s2 )
1056+ except ValueError as e :
1057+ return False
1058+
1059+ if len (s1 ) != len (s2 ):
1060+ return False
1061+
1062+ if indexed not in [True , False ]:
1063+ indexed = True
1064+
1065+ if indexed :
1066+ xOK = abs (s1 [0 ].x () - s2 [0 ].x ()) < CN .TOL
1067+ yOK = abs (s1 [0 ].y () - s2 [0 ].y ()) < CN .TOL
1068+ zOK = abs (s1 [0 ].z () - s2 [0 ].z ()) < CN .TOL
1069+
1070+ if xOK and yOK and zOK and len (s1 ) == 1 :
1071+ return True
1072+ else :
1073+ indx = None
1074+
1075+ for i , pt in enumerate (s2 ):
1076+ if indx : break
1077+
1078+ xOK = abs (s1 [0 ].x () - s2 [i ].x ()) < CN .TOL
1079+ yOK = abs (s1 [0 ].y () - s2 [i ].y ()) < CN .TOL
1080+ zOK = abs (s1 [0 ].z () - s2 [i ].z ()) < CN .TOL
1081+
1082+ if xOK and yOK and zOK : indx = i
1083+
1084+ if not indx : return False
1085+
1086+ s2 = collections .deque (s2 )
1087+ s2 .rotate (indx )
1088+ s2 = list (s2 )
1089+
1090+ # openstudio.isAlmostEqual3dPt(p1, p2, TOL) # ... from v350 onwards.
1091+ for i in range (len (s1 )):
1092+ xOK = abs (s1 [i ].x () - s2 [i ].x ()) < CN .TOL
1093+ yOK = abs (s1 [i ].y () - s2 [i ].y ()) < CN .TOL
1094+ zOK = abs (s1 [i ].z () - s2 [i ].z ()) < CN .TOL
1095+
1096+ if not xOK or not yOK or not zOK : return False
1097+
1098+ return True
0 commit comments