Skip to content

Commit 4091336

Browse files
committed
Tests basic point3D methods
1 parent 9006151 commit 4091336

File tree

2 files changed

+263
-55
lines changed

2 files changed

+263
-55
lines changed

src/osut/osut.py

Lines changed: 255 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,22 @@
2828
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929

3030
import re
31+
import math
32+
import collections
3133
import openstudio
3234
from oslg import oslg
3335
from dataclasses import dataclass
3436

3537
@dataclass(frozen=True)
3638
class _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)
4347
CN = _CN()
4448

4549
# General surface orientations (see 'facets' method).
@@ -207,15 +211,15 @@ def uo() -> dict:
207211

208212

209213
def 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

238242
def 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

271275
def 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

346350
def 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

409411
def 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

Comments
 (0)