osut 0.2.7 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +20 -4
- data/.gitignore +2 -0
- data/LICENSE +1 -1
- data/lib/osut/utils.rb +675 -29
- data/lib/osut/version.rb +2 -2
- data/lib/osut.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2491bf7ed2ed4277ccfe4087a661660e720bd7f417b1e51f3c1f81715e53193f
|
4
|
+
data.tar.gz: 7c07191bf8cb725fdb9a04e15269aa6431fd7aa66955b9b99b2c1d61929033c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed0ad6091cf8bd858a1b3de7c17b4e13fd21816871c1dc8bf539d4b7b03f232dfa1621d3e6a839eebde9a779d7562e9c56e04bf10a52c417d884ebba7128e9c0
|
7
|
+
data.tar.gz: 5be1c0c83a39becabf70d6202916b4012ca1923b2102084515dfccae765e9fffc5e4db27787523a1a7694a852e15029176216d1fff255e0653baba97c399eacd
|
@@ -7,7 +7,7 @@ on:
|
|
7
7
|
|
8
8
|
jobs:
|
9
9
|
test_300x:
|
10
|
-
runs-on: ubuntu-
|
10
|
+
runs-on: ubuntu-22.04
|
11
11
|
steps:
|
12
12
|
- name: Check out repository
|
13
13
|
uses: actions/checkout@v2
|
@@ -23,7 +23,7 @@ jobs:
|
|
23
23
|
docker exec -t test bundle exec rake
|
24
24
|
docker kill test
|
25
25
|
test_321x:
|
26
|
-
runs-on: ubuntu-
|
26
|
+
runs-on: ubuntu-22.04
|
27
27
|
steps:
|
28
28
|
- name: Check out repository
|
29
29
|
uses: actions/checkout@v2
|
@@ -39,7 +39,7 @@ jobs:
|
|
39
39
|
docker exec -t test bundle exec rake
|
40
40
|
docker kill test
|
41
41
|
test_330x:
|
42
|
-
runs-on: ubuntu-
|
42
|
+
runs-on: ubuntu-22.04
|
43
43
|
steps:
|
44
44
|
- name: Check out repository
|
45
45
|
uses: actions/checkout@v2
|
@@ -55,7 +55,7 @@ jobs:
|
|
55
55
|
docker exec -t test bundle exec rake
|
56
56
|
docker kill test
|
57
57
|
test_340x:
|
58
|
-
runs-on: ubuntu-
|
58
|
+
runs-on: ubuntu-22.04
|
59
59
|
steps:
|
60
60
|
- name: Check out repository
|
61
61
|
uses: actions/checkout@v2
|
@@ -70,3 +70,19 @@ jobs:
|
|
70
70
|
docker exec -t test bundle update
|
71
71
|
docker exec -t test bundle exec rake
|
72
72
|
docker kill test
|
73
|
+
test_351x:
|
74
|
+
runs-on: ubuntu-22.04
|
75
|
+
steps:
|
76
|
+
- name: Check out repository
|
77
|
+
uses: actions/checkout@v2
|
78
|
+
- name: Run Tests
|
79
|
+
run: |
|
80
|
+
echo $(pwd)
|
81
|
+
echo $(ls)
|
82
|
+
docker pull nrel/openstudio:3.5.1
|
83
|
+
docker run --name test --rm -d -t -v $(pwd):/work -w /work nrel/openstudio:3.5.1
|
84
|
+
docker exec -t test pwd
|
85
|
+
docker exec -t test ls
|
86
|
+
docker exec -t test bundle update
|
87
|
+
docker exec -t test bundle exec rake
|
88
|
+
docker kill test
|
data/.gitignore
CHANGED
data/LICENSE
CHANGED
data/lib/osut/utils.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# BSD 3-Clause License
|
2
2
|
#
|
3
|
-
# Copyright (c) 2022, Denis Bourgeois
|
3
|
+
# Copyright (c) 2022-2023, Denis Bourgeois
|
4
4
|
# All rights reserved.
|
5
5
|
#
|
6
6
|
# Redistribution and use in source and binary forms, with or without
|
@@ -41,6 +41,8 @@ module OSut
|
|
41
41
|
ERR = OSut::ERROR # flag invalid .osm inputs (then exit via 'return')
|
42
42
|
FTL = OSut::FATAL # not currently used in OSut
|
43
43
|
NS = "nameString" # OpenStudio IdfObject nameString method
|
44
|
+
HEAD = 2.032 # standard 80" door
|
45
|
+
SILL = 0.762 # standard 30" window sill
|
44
46
|
|
45
47
|
# This first set of utilities (~750 lines) help distinguishing spaces that
|
46
48
|
# are directly vs indirectly CONDITIONED, vs SEMI-HEATED. The solution here
|
@@ -59,7 +61,7 @@ module OSut
|
|
59
61
|
# cooling system of sufficient size to maintain temperatures suitable
|
60
62
|
# for HUMAN COMFORT:
|
61
63
|
# - COOLED: cooled by a system >= 10 W/m2
|
62
|
-
# - HEATED: heated by a system e.g
|
64
|
+
# - HEATED: heated by a system, e.g. >= 50 W/m2 in Climate Zone CZ-7
|
63
65
|
# - INDIRECTLY: heated or cooled via adjacent space(s) provided:
|
64
66
|
# - UA of adjacent surfaces > UA of other surfaces
|
65
67
|
# or
|
@@ -89,7 +91,7 @@ module OSut
|
|
89
91
|
# response to the exterior ambient temperature by the provision, either
|
90
92
|
# DIRECTLY or INDIRECTLY, of heating or cooling [...]". Although criteria
|
91
93
|
# differ (e.g., not sizing-based), the general idea is sufficiently similar
|
92
|
-
# to ASHRAE 90.1 (e.g
|
94
|
+
# to ASHRAE 90.1 (e.g. heating and/or cooling based, no distinction for
|
93
95
|
# INDIRECTLY conditioned spaces like plenums).
|
94
96
|
#
|
95
97
|
# SEMI-HEATED spaces are also a defined NECB term, but again the distinction
|
@@ -109,16 +111,16 @@ module OSut
|
|
109
111
|
# processes. As discussed in greater detail elswhere, methods are developed to
|
110
112
|
# rely on zoning info and/or "intended" temperature setpoints.
|
111
113
|
#
|
112
|
-
# For an OpenStudio model
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
116
|
-
#
|
114
|
+
# For an OpenStudio model in an incomplete or preliminary state, e.g. holding
|
115
|
+
# fully-formed ENCLOSED spaces without thermal zoning information or setpoint
|
116
|
+
# temperatures (early design stage assessments of form, porosity or envelope),
|
117
|
+
# all OpenStudio spaces will be considered CONDITIONED, presuming setpoints of
|
118
|
+
# ~21°C (heating) and ~24°C (cooling).
|
117
119
|
#
|
118
|
-
# If ANY valid space/zone-specific temperature setpoints are found in the
|
119
|
-
# spaces/zones WITHOUT valid heating or cooling setpoints
|
120
|
-
# UNCONDITIONED or UNENCLOSED spaces (like attics), or
|
121
|
-
# spaces (like plenums), see "plenum?" method.
|
120
|
+
# If ANY valid space/zone-specific temperature setpoints are found in the
|
121
|
+
# OpenStudio model, spaces/zones WITHOUT valid heating or cooling setpoints
|
122
|
+
# are considered as UNCONDITIONED or UNENCLOSED spaces (like attics), or
|
123
|
+
# INDIRECTLY CONDITIONED spaces (like plenums), see "plenum?" method.
|
122
124
|
|
123
125
|
##
|
124
126
|
# Return min & max values of a schedule (ruleset).
|
@@ -139,6 +141,7 @@ module OSut
|
|
139
141
|
res = { min: nil, max: nil }
|
140
142
|
|
141
143
|
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
144
|
+
|
142
145
|
id = sched.nameString
|
143
146
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
144
147
|
|
@@ -186,6 +189,7 @@ module OSut
|
|
186
189
|
res = { min: nil, max: nil }
|
187
190
|
|
188
191
|
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
192
|
+
|
189
193
|
id = sched.nameString
|
190
194
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
191
195
|
|
@@ -218,6 +222,7 @@ module OSut
|
|
218
222
|
res = { min: nil, max: nil }
|
219
223
|
|
220
224
|
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
225
|
+
|
221
226
|
id = sched.nameString
|
222
227
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
223
228
|
|
@@ -231,9 +236,11 @@ module OSut
|
|
231
236
|
end
|
232
237
|
|
233
238
|
return empty("'#{id}' values", mth, ERR, res) if vals.empty?
|
239
|
+
|
234
240
|
ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
|
235
241
|
log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok
|
236
242
|
return res unless ok
|
243
|
+
|
237
244
|
res[:min] = vals.min
|
238
245
|
res[:max] = vals.max
|
239
246
|
|
@@ -248,19 +255,21 @@ module OSut
|
|
248
255
|
# @return [Hash] min: (Float), max: (Float)
|
249
256
|
# @return [Hash] min: nil, max: nil (if invalid input)
|
250
257
|
def scheduleIntervalMinMax(sched = nil)
|
251
|
-
mth
|
252
|
-
cl
|
253
|
-
vals
|
254
|
-
|
255
|
-
res = { min: nil, max: nil }
|
258
|
+
mth = "OSut::#{__callee__}"
|
259
|
+
cl = OpenStudio::Model::ScheduleInterval
|
260
|
+
vals = []
|
261
|
+
res = { min: nil, max: nil }
|
256
262
|
|
257
263
|
return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
|
264
|
+
|
258
265
|
id = sched.nameString
|
259
266
|
return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
|
267
|
+
|
260
268
|
vals = sched.timeSeries.values
|
261
|
-
ok
|
269
|
+
ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
|
262
270
|
log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok
|
263
271
|
return res unless ok
|
272
|
+
|
264
273
|
res[:min] = vals.min
|
265
274
|
res[:max] = vals.max
|
266
275
|
|
@@ -289,6 +298,7 @@ module OSut
|
|
289
298
|
res = { spt: nil, dual: false }
|
290
299
|
|
291
300
|
return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
|
301
|
+
|
292
302
|
id = zone.nameString
|
293
303
|
return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
|
294
304
|
|
@@ -369,6 +379,7 @@ module OSut
|
|
369
379
|
end
|
370
380
|
|
371
381
|
return res if zone.thermostat.empty?
|
382
|
+
|
372
383
|
tstat = zone.thermostat.get
|
373
384
|
res[:spt] = nil
|
374
385
|
|
@@ -427,6 +438,7 @@ module OSut
|
|
427
438
|
|
428
439
|
sched.getScheduleWeeks.each do |week|
|
429
440
|
next if week.winterDesignDaySchedule.empty?
|
441
|
+
|
430
442
|
dd = week.winterDesignDaySchedule.get
|
431
443
|
next unless dd.values.empty?
|
432
444
|
|
@@ -479,6 +491,7 @@ module OSut
|
|
479
491
|
res = { spt: nil, dual: false }
|
480
492
|
|
481
493
|
return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
|
494
|
+
|
482
495
|
id = zone.nameString
|
483
496
|
return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
|
484
497
|
|
@@ -546,6 +559,7 @@ module OSut
|
|
546
559
|
end
|
547
560
|
|
548
561
|
return res if zone.thermostat.empty?
|
562
|
+
|
549
563
|
tstat = zone.thermostat.get
|
550
564
|
res[:spt] = nil
|
551
565
|
|
@@ -604,6 +618,7 @@ module OSut
|
|
604
618
|
|
605
619
|
sched.getScheduleWeeks.each do |week|
|
606
620
|
next if week.summerDesignDaySchedule.empty?
|
621
|
+
|
607
622
|
dd = week.summerDesignDaySchedule.get
|
608
623
|
next unless dd.values.empty?
|
609
624
|
|
@@ -691,10 +706,13 @@ module OSut
|
|
691
706
|
cl = OpenStudio::Model::Space
|
692
707
|
|
693
708
|
return invalid("space", mth, 1, DBG, false) unless space.respond_to?(NS)
|
709
|
+
|
694
710
|
id = space.nameString
|
695
711
|
return mismatch(id, space, cl, mth, DBG, false) unless space.is_a?(cl)
|
712
|
+
|
696
713
|
valid = loops == true || loops == false
|
697
714
|
return invalid("loops", mth, 2, DBG, false) unless valid
|
715
|
+
|
698
716
|
valid = setpoints == true || setpoints == false
|
699
717
|
return invalid("setpoints", mth, 3, DBG, false) unless valid
|
700
718
|
|
@@ -714,11 +732,11 @@ module OSut
|
|
714
732
|
unless space.spaceType.empty?
|
715
733
|
type = space.spaceType.get
|
716
734
|
return type.nameString.downcase == "plenum" # C
|
735
|
+
end
|
717
736
|
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
end
|
737
|
+
unless type.standardsSpaceType.empty?
|
738
|
+
type = type.standardsSpaceType.get
|
739
|
+
return type.downcase == "plenum" # C
|
722
740
|
end
|
723
741
|
|
724
742
|
false
|
@@ -751,6 +769,7 @@ module OSut
|
|
751
769
|
next unless l.numericType.get.downcase == "discrete"
|
752
770
|
next unless l.unitType.downcase == "availability"
|
753
771
|
next unless l.nameString.downcase == "hvac operation scheduletypelimits"
|
772
|
+
|
754
773
|
limits = l
|
755
774
|
end
|
756
775
|
|
@@ -771,6 +790,7 @@ module OSut
|
|
771
790
|
# Seasonal availability start/end dates.
|
772
791
|
year = model.yearDescription
|
773
792
|
return empty("yearDescription", mth, ERR) if year.empty?
|
793
|
+
|
774
794
|
year = year.get
|
775
795
|
may01 = year.makeDate(OpenStudio::MonthOfYear.new("May"), 1)
|
776
796
|
oct31 = year.makeDate(OpenStudio::MonthOfYear.new("Oct"), 31)
|
@@ -850,9 +870,11 @@ module OSut
|
|
850
870
|
ok = schedule.setScheduleTypeLimits(limits)
|
851
871
|
log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})") unless ok
|
852
872
|
return nil unless ok
|
873
|
+
|
853
874
|
ok = schedule.defaultDaySchedule.addValue(time, val)
|
854
875
|
log(ERR, "'#{nom}': Can't set default day schedule (#{mth})") unless ok
|
855
876
|
return nil unless ok
|
877
|
+
|
856
878
|
schedule.defaultDaySchedule.setName(dft)
|
857
879
|
|
858
880
|
unless tag.empty?
|
@@ -861,12 +883,15 @@ module OSut
|
|
861
883
|
ok = rule.setStartDate(may01)
|
862
884
|
log(ERR, "'#{tag}': Can't set start date (#{mth})") unless ok
|
863
885
|
return nil unless ok
|
886
|
+
|
864
887
|
ok = rule.setEndDate(oct31)
|
865
888
|
log(ERR, "'#{tag}': Can't set end date (#{mth})") unless ok
|
866
889
|
return nil unless ok
|
890
|
+
|
867
891
|
ok = rule.setApplyAllDays(true)
|
868
892
|
log(ERR, "'#{tag}': Can't apply to all days (#{mth})") unless ok
|
869
893
|
return nil unless ok
|
894
|
+
|
870
895
|
rule.daySchedule.setName(day)
|
871
896
|
end
|
872
897
|
|
@@ -874,7 +899,7 @@ module OSut
|
|
874
899
|
end
|
875
900
|
|
876
901
|
##
|
877
|
-
# Validate if default construction set holds a base
|
902
|
+
# Validate if default construction set holds a base construction.
|
878
903
|
#
|
879
904
|
# @param set [OpenStudio::Model::DefaultConstructionSet] a default set
|
880
905
|
# @param bse [OpensStudio::Model::ConstructionBase] a construction base
|
@@ -890,17 +915,23 @@ module OSut
|
|
890
915
|
cl2 = OpenStudio::Model::ConstructionBase
|
891
916
|
|
892
917
|
return invalid("set", mth, 1, DBG, false) unless set.respond_to?(NS)
|
918
|
+
|
893
919
|
id = set.nameString
|
894
920
|
return mismatch(id, set, cl1, mth, DBG, false) unless set.is_a?(cl1)
|
895
921
|
return invalid("base", mth, 2, DBG, false) unless bse.respond_to?(NS)
|
922
|
+
|
896
923
|
id = bse.nameString
|
897
924
|
return mismatch(id, bse, cl2, mth, DBG, false) unless bse.is_a?(cl2)
|
925
|
+
|
898
926
|
valid = gr == true || gr == false
|
899
927
|
return invalid("ground", mth, 3, DBG, false) unless valid
|
928
|
+
|
900
929
|
valid = ex == true || ex == false
|
901
930
|
return invalid("exterior", mth, 4, DBG, false) unless valid
|
931
|
+
|
902
932
|
valid = typ.respond_to?(:to_s)
|
903
933
|
return invalid("surface typ", mth, 4, DBG, false) unless valid
|
934
|
+
|
904
935
|
type = typ.to_s.downcase
|
905
936
|
valid = type == "floor" || type == "wall" || type == "roofceiling"
|
906
937
|
return invalid("surface type", mth, 5, DBG, false) unless valid
|
@@ -959,6 +990,7 @@ module OSut
|
|
959
990
|
|
960
991
|
return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
|
961
992
|
return invalid("s", mth, 2) unless s.respond_to?(NS)
|
993
|
+
|
962
994
|
id = s.nameString
|
963
995
|
return mismatch(id, s, cl2, mth) unless s.is_a?(cl2)
|
964
996
|
|
@@ -966,8 +998,10 @@ module OSut
|
|
966
998
|
log(ERR, "'#{id}' construction not defaulted (#{mth})") unless ok
|
967
999
|
return nil unless ok
|
968
1000
|
return empty("'#{id}' construction", mth, ERR) if s.construction.empty?
|
1001
|
+
|
969
1002
|
base = s.construction.get
|
970
1003
|
return empty("'#{id}' space", mth, ERR) if s.space.empty?
|
1004
|
+
|
971
1005
|
space = s.space.get
|
972
1006
|
type = s.surfaceType
|
973
1007
|
ground = false
|
@@ -1043,12 +1077,14 @@ module OSut
|
|
1043
1077
|
cl = OpenStudio::Model::LayeredConstruction
|
1044
1078
|
|
1045
1079
|
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
1080
|
+
|
1046
1081
|
id = lc.nameString
|
1047
1082
|
return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
|
1048
1083
|
|
1049
1084
|
ok = standardOpaqueLayers?(lc)
|
1050
1085
|
log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
|
1051
1086
|
return 0.0 unless ok
|
1087
|
+
|
1052
1088
|
thickness = 0.0
|
1053
1089
|
lc.layers.each { |m| thickness += m.thickness }
|
1054
1090
|
|
@@ -1113,11 +1149,14 @@ module OSut
|
|
1113
1149
|
cl2 = Numeric
|
1114
1150
|
|
1115
1151
|
return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
|
1152
|
+
|
1116
1153
|
id = lc.nameString
|
1154
|
+
|
1117
1155
|
return mismatch(id, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
|
1118
1156
|
return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
|
1119
1157
|
return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
|
1120
|
-
|
1158
|
+
|
1159
|
+
t += 273.0 # °C to K
|
1121
1160
|
return negative("temp K", mth, DBG, 0.0) if t < 0
|
1122
1161
|
return negative("film", mth, DBG, 0.0) if film < 0
|
1123
1162
|
|
@@ -1127,6 +1166,7 @@ module OSut
|
|
1127
1166
|
# Fenestration materials first (ignoring shades, screens, etc.)
|
1128
1167
|
empty = m.to_SimpleGlazing.empty?
|
1129
1168
|
return 1 / m.to_SimpleGlazing.get.uFactor unless empty
|
1169
|
+
|
1130
1170
|
empty = m.to_StandardGlazing.empty?
|
1131
1171
|
rsi += m.to_StandardGlazing.get.thermalResistance unless empty
|
1132
1172
|
empty = m.to_RefractionExtinctionGlazing.empty?
|
@@ -1167,6 +1207,7 @@ module OSut
|
|
1167
1207
|
i = 0 # iterator
|
1168
1208
|
|
1169
1209
|
return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
|
1210
|
+
|
1170
1211
|
id = lc.nameString
|
1171
1212
|
return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
|
1172
1213
|
|
@@ -1221,6 +1262,7 @@ module OSut
|
|
1221
1262
|
|
1222
1263
|
return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
|
1223
1264
|
return invalid("group", mth, 2, DBG, res) unless group.respond_to?(NS)
|
1265
|
+
|
1224
1266
|
id = group.nameString
|
1225
1267
|
return mismatch(id, group, cl2, mth, DBG, res) unless group.is_a?(cl2)
|
1226
1268
|
|
@@ -1244,10 +1286,10 @@ module OSut
|
|
1244
1286
|
cl2 = Numeric
|
1245
1287
|
|
1246
1288
|
return mismatch("vector", v, cl1, mth, DBG, v) unless v.is_a?(cl1)
|
1247
|
-
return mismatch("x",
|
1248
|
-
return mismatch("y",
|
1249
|
-
return mismatch("z",
|
1250
|
-
return mismatch("m",
|
1289
|
+
return mismatch("x", v.x, cl2, mth, DBG, v) unless v.x.respond_to?(:to_f)
|
1290
|
+
return mismatch("y", v.y, cl2, mth, DBG, v) unless v.y.respond_to?(:to_f)
|
1291
|
+
return mismatch("z", v.z, cl2, mth, DBG, v) unless v.z.respond_to?(:to_f)
|
1292
|
+
return mismatch("m", m, cl2, mth, DBG, v) unless m.respond_to?(:to_f)
|
1251
1293
|
|
1252
1294
|
OpenStudio::Vector3d.new(m * v.x, m * v.y, m * v.z)
|
1253
1295
|
end
|
@@ -1266,6 +1308,7 @@ module OSut
|
|
1266
1308
|
|
1267
1309
|
valid = pts.is_a?(cl1) || pts.is_a?(Array)
|
1268
1310
|
return mismatch("points", pts, cl1, mth, DBG, v) unless valid
|
1311
|
+
|
1269
1312
|
pts.each { |pt| mismatch("pt", pt, cl2, mth, ERR, v) unless pt.is_a?(cl2) }
|
1270
1313
|
pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, 0) }
|
1271
1314
|
|
@@ -1311,23 +1354,29 @@ module OSut
|
|
1311
1354
|
ft = OpenStudio::Transformation.alignFace(p1)
|
1312
1355
|
ft_p1 = flatZ( (ft.inverse * p1) )
|
1313
1356
|
return false if ft_p1.empty?
|
1357
|
+
|
1314
1358
|
cw = OpenStudio.pointInPolygon(ft_p1.first, ft_p1, TOL)
|
1315
1359
|
ft_p1 = flatZ( (ft.inverse * p1).reverse ) unless cw
|
1316
1360
|
ft_p2 = flatZ( (ft.inverse * p2).reverse ) unless cw
|
1317
1361
|
ft_p2 = flatZ( (ft.inverse * p2) ) if cw
|
1318
1362
|
return false if ft_p2.empty?
|
1363
|
+
|
1319
1364
|
area1 = OpenStudio.getArea(ft_p1)
|
1320
1365
|
area2 = OpenStudio.getArea(ft_p2)
|
1321
1366
|
return empty("#{i1} area", mth, ERR, a) if area1.empty?
|
1322
1367
|
return empty("#{i2} area", mth, ERR, a) if area2.empty?
|
1368
|
+
|
1323
1369
|
area1 = area1.get
|
1324
1370
|
area2 = area2.get
|
1325
1371
|
union = OpenStudio.join(ft_p1, ft_p2, TOL2)
|
1326
1372
|
return false if union.empty?
|
1373
|
+
|
1327
1374
|
union = union.get
|
1328
1375
|
area = OpenStudio.getArea(union)
|
1329
1376
|
return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty?
|
1377
|
+
|
1330
1378
|
area = area.get
|
1379
|
+
|
1331
1380
|
return false if area < TOL
|
1332
1381
|
return true if (area - area2).abs < TOL
|
1333
1382
|
return false if (area - area2).abs > TOL
|
@@ -1376,25 +1425,33 @@ module OSut
|
|
1376
1425
|
ft_p2 = flatZ( (ft.inverse * p2) )
|
1377
1426
|
return false if ft_p1.empty?
|
1378
1427
|
return false if ft_p2.empty?
|
1428
|
+
|
1379
1429
|
cw = OpenStudio.pointInPolygon(ft_p1.first, ft_p1, TOL)
|
1380
1430
|
ft_p1 = flatZ( (ft.inverse * p1).reverse ) unless cw
|
1381
1431
|
ft_p2 = flatZ( (ft.inverse * p2).reverse ) unless cw
|
1382
1432
|
return false if ft_p1.empty?
|
1383
1433
|
return false if ft_p2.empty?
|
1434
|
+
|
1384
1435
|
area1 = OpenStudio.getArea(ft_p1)
|
1385
1436
|
area2 = OpenStudio.getArea(ft_p2)
|
1386
1437
|
return empty("#{i1} area", mth, ERR, a) if area1.empty?
|
1387
1438
|
return empty("#{i2} area", mth, ERR, a) if area2.empty?
|
1439
|
+
|
1388
1440
|
area1 = area1.get
|
1389
1441
|
area2 = area2.get
|
1390
1442
|
union = OpenStudio.join(ft_p1, ft_p2, TOL2)
|
1391
1443
|
return false if union.empty?
|
1444
|
+
|
1392
1445
|
union = union.get
|
1393
1446
|
area = OpenStudio.getArea(union)
|
1394
1447
|
return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty?
|
1448
|
+
|
1395
1449
|
area = area.get
|
1396
1450
|
return false if area < TOL
|
1397
1451
|
|
1452
|
+
delta = (area - area1 - area2).abs
|
1453
|
+
return false if delta < TOL
|
1454
|
+
|
1398
1455
|
true
|
1399
1456
|
end
|
1400
1457
|
|
@@ -1408,19 +1465,22 @@ module OSut
|
|
1408
1465
|
# @return [OpenStudio::Point3dVector] offset points if successful
|
1409
1466
|
# @return [OpenStudio::Point3dVector] original points if invalid input
|
1410
1467
|
def offset(p1 = [], w = 0, v = 0)
|
1411
|
-
mth = "
|
1468
|
+
mth = "OSut::#{__callee__}"
|
1412
1469
|
cl = OpenStudio::Point3d
|
1413
1470
|
vrsn = OpenStudio.openStudioVersion.split(".").map(&:to_i).join.to_i
|
1414
1471
|
|
1415
1472
|
valid = p1.is_a?(OpenStudio::Point3dVector) || p1.is_a?(Array)
|
1416
1473
|
return mismatch("pts", p1, cl1, mth, DBG, p1) unless valid
|
1417
1474
|
return empty("pts", mth, ERR, p1) if p1.empty?
|
1475
|
+
|
1418
1476
|
valid = p1.size == 3 || p1.size == 4
|
1419
1477
|
iv = true if p1.size == 4
|
1420
1478
|
return invalid("pts", mth, 1, DBG, p1) unless valid
|
1421
1479
|
return invalid("width", mth, 2, DBG, p1) unless w.respond_to?(:to_f)
|
1480
|
+
|
1422
1481
|
w = w.to_f
|
1423
1482
|
return p1 if w < 0.0254
|
1483
|
+
|
1424
1484
|
v = v.to_i if v.respond_to?(:to_i)
|
1425
1485
|
v = 0 unless v.respond_to?(:to_i)
|
1426
1486
|
v = vrsn if v.zero?
|
@@ -1432,16 +1492,19 @@ module OSut
|
|
1432
1492
|
ft = OpenStudio::Transformation::alignFace(p1)
|
1433
1493
|
ft_pts = flatZ( (ft.inverse * p1) )
|
1434
1494
|
return p1 if ft_pts.empty?
|
1495
|
+
|
1435
1496
|
cw = OpenStudio::pointInPolygon(ft_pts.first, ft_pts, TOL)
|
1436
1497
|
ft_pts = flatZ( (ft.inverse * p1).reverse ) unless cw
|
1437
1498
|
offset = OpenStudio.buffer(ft_pts, w, TOL)
|
1438
1499
|
return p1 if offset.empty?
|
1500
|
+
|
1439
1501
|
offset = offset.get
|
1440
1502
|
offset = ft * offset if cw
|
1441
1503
|
offset = (ft * offset).reverse unless cw
|
1442
1504
|
|
1443
1505
|
pz = OpenStudio::Point3dVector.new
|
1444
1506
|
offset.each { |o| pz << OpenStudio::Point3d.new(o.x, o.y, o.z ) }
|
1507
|
+
|
1445
1508
|
return pz
|
1446
1509
|
else # brute force approach
|
1447
1510
|
pz = {}
|
@@ -1626,6 +1689,589 @@ module OSut
|
|
1626
1689
|
end
|
1627
1690
|
end
|
1628
1691
|
|
1692
|
+
##
|
1693
|
+
# Validate whether an OpenStudio planar surface is safe to process.
|
1694
|
+
#
|
1695
|
+
# @param s [OpenStudio::Model::PlanarSurface] a surface
|
1696
|
+
#
|
1697
|
+
# @return [Bool] true if valid surface
|
1698
|
+
def surface_valid?(s = nil)
|
1699
|
+
mth = "OSut::#{__callee__}"
|
1700
|
+
cl = OpenStudio::Model::PlanarSurface
|
1701
|
+
|
1702
|
+
return mismatch("surface", s, cl, mth, DBG, false) unless s.is_a?(cl)
|
1703
|
+
|
1704
|
+
id = s.nameString
|
1705
|
+
size = s.vertices.size
|
1706
|
+
last = size - 1
|
1707
|
+
|
1708
|
+
log(ERR, "#{id} #{size} vertices? need +3 (#{mth})") unless size > 2
|
1709
|
+
return false unless size > 2
|
1710
|
+
|
1711
|
+
[0, last].each do |i|
|
1712
|
+
v1 = s.vertices[i]
|
1713
|
+
v2 = s.vertices[i + 1] unless i == last
|
1714
|
+
v2 = s.vertices.first if i == last
|
1715
|
+
vec = v2 - v1
|
1716
|
+
bad = vec.length < TOL
|
1717
|
+
|
1718
|
+
# As is, this comparison also catches collinear vertices (< 10mm apart)
|
1719
|
+
# along an edge. Should avoid red-flagging such cases. TO DO.
|
1720
|
+
log(ERR, "#{id}: < #{TOL}m (#{mth})") if bad
|
1721
|
+
return false if bad
|
1722
|
+
end
|
1723
|
+
|
1724
|
+
# Add as many extra tests as needed ...
|
1725
|
+
true
|
1726
|
+
end
|
1727
|
+
|
1728
|
+
##
|
1729
|
+
# Add sub surfaces (e.g. windows, doors, skylights) to surface.
|
1730
|
+
#
|
1731
|
+
# @param model [OpenStudio::Model::Model] a model
|
1732
|
+
# @param s [OpenStudio::Model::Surface] a model surface
|
1733
|
+
# @param subs [Array] requested sub surface attributes
|
1734
|
+
# @param clear [Bool] remove current sub surfaces if true
|
1735
|
+
# @param bfr [Double] safety buffer (m), when ~aligned along other edges
|
1736
|
+
#
|
1737
|
+
# @return [Bool] true if successful (check for logged messages if failures)
|
1738
|
+
def addSubs(model = nil, s = nil, subs = [], clear = false, bfr = 0.005)
|
1739
|
+
mth = "OSut::#{__callee__}"
|
1740
|
+
v = OpenStudio.openStudioVersion.split(".").join.to_i
|
1741
|
+
cl1 = OpenStudio::Model::Model
|
1742
|
+
cl2 = OpenStudio::Model::Surface
|
1743
|
+
cl3 = Array
|
1744
|
+
cl4 = Hash
|
1745
|
+
cl5 = Numeric
|
1746
|
+
min = 0.050 # minimum ratio value ( 5%)
|
1747
|
+
max = 0.950 # maximum ratio value (95%)
|
1748
|
+
no = false
|
1749
|
+
|
1750
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1751
|
+
# Exit if mismatched or invalid argument classes.
|
1752
|
+
return mismatch("model", model, cl1, mth, DBG, no) unless model.is_a?(cl1)
|
1753
|
+
return mismatch("surface", s, cl2, mth, DBG, no) unless s.is_a?(cl2)
|
1754
|
+
return mismatch("subs", subs, cl3, mth, DBG, no) unless subs.is_a?(cl3)
|
1755
|
+
return no unless surface_valid?(s)
|
1756
|
+
|
1757
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1758
|
+
# Clear existing sub surfaces if requested.
|
1759
|
+
nom = s.nameString
|
1760
|
+
|
1761
|
+
unless clear == true || clear == false
|
1762
|
+
log(WRN, "#{nom}: Keeping existing sub surfaces (#{mth})")
|
1763
|
+
clear = false
|
1764
|
+
end
|
1765
|
+
|
1766
|
+
s.subSurfaces.map(&:remove) if clear
|
1767
|
+
|
1768
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1769
|
+
# Allowable sub surface types ... & Frame&Divider enabled
|
1770
|
+
# - "FixedWindow" | true
|
1771
|
+
# - "OperableWindow" | true
|
1772
|
+
# - "Door" | false
|
1773
|
+
# - "GlassDoor" | true
|
1774
|
+
# - "OverheadDoor" | false
|
1775
|
+
# - "Skylight" | false if v < 321
|
1776
|
+
# - "TubularDaylightDome" | false
|
1777
|
+
# - "TubularDaylightDiffuser" | false
|
1778
|
+
type = "FixedWindow"
|
1779
|
+
types = OpenStudio::Model::SubSurface.validSubSurfaceTypeValues
|
1780
|
+
stype = s.surfaceType # Wall, RoofCeiling or Floor
|
1781
|
+
|
1782
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1783
|
+
# Fetch transform, as if host surface vertices were to "align", i.e.:
|
1784
|
+
# - rotated/tilted ... then flattened along XY plane
|
1785
|
+
# - all Z-axis coordinates ~= 0
|
1786
|
+
# - vertices with the lowest X-axis values are "aligned" along X-axis (0)
|
1787
|
+
# - vertices with the lowest Z-axis values are "aligned" along Y-axis (0)
|
1788
|
+
# - Z-axis values are represented as Y-axis values
|
1789
|
+
tr = OpenStudio::Transformation.alignFace(s.vertices)
|
1790
|
+
|
1791
|
+
# Aligned vertices of host surface, and fetch attributes.
|
1792
|
+
aligned = tr.inverse * s.vertices
|
1793
|
+
max_x = aligned.max_by(&:x).x
|
1794
|
+
max_y = aligned.max_by(&:y).y
|
1795
|
+
mid_x = max_x / 2
|
1796
|
+
mid_y = max_y / 2
|
1797
|
+
|
1798
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1799
|
+
# Assign default values to certain sub keys (if missing), +more validation.
|
1800
|
+
subs.each_with_index do |sub, index|
|
1801
|
+
return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl4)
|
1802
|
+
|
1803
|
+
# Required key:value pairs (either set by the user or defaulted).
|
1804
|
+
sub[:id ] = "" unless sub.key?(:id ) # "Window 007"
|
1805
|
+
sub[:type ] = type unless sub.key?(:type ) # "FixedWindow"
|
1806
|
+
sub[:count ] = 1 unless sub.key?(:count ) # for an array
|
1807
|
+
sub[:multiplier] = 1 unless sub.key?(:multiplier)
|
1808
|
+
sub[:frame ] = nil unless sub.key?(:frame ) # frame/divider
|
1809
|
+
sub[:assembly ] = nil unless sub.key?(:assembly ) # construction
|
1810
|
+
|
1811
|
+
# Optional key:value pairs.
|
1812
|
+
# sub[:ratio ] # e.g. %FWR
|
1813
|
+
# sub[:head ] # e.g. std 80" door + frame/buffers (+ m)
|
1814
|
+
# sub[:sill ] # e.g. std 30" sill + frame/buffers (+ m)
|
1815
|
+
# sub[:height ] # any sub surface height, below "head" (+ m)
|
1816
|
+
# sub[:width ] # e.g. 1.200 m
|
1817
|
+
# sub[:offset ] # if array (+ m)
|
1818
|
+
# sub[:centreline] # left or right of base surface centreline (+/- m)
|
1819
|
+
# sub[:r_buffer ] # buffer between sub/array and right-side corner (+ m)
|
1820
|
+
# sub[:l_buffer ] # buffer between sub/array and left-side corner (+ m)
|
1821
|
+
|
1822
|
+
sub[:id] = "#{nom}|#{index}" if sub[:id].empty?
|
1823
|
+
id = sub[:id]
|
1824
|
+
|
1825
|
+
# If sub surface type is invalid, log/reset. Additional corrections may
|
1826
|
+
# be enabled once a sub surface is actually instantiated.
|
1827
|
+
unless types.include?(sub[:type])
|
1828
|
+
log(WRN, "Reset invalid '#{id}' type to '#{type}' (#{mth})")
|
1829
|
+
sub[:type] = type
|
1830
|
+
end
|
1831
|
+
|
1832
|
+
# Log/ignore (optional) frame & divider object.
|
1833
|
+
unless sub[:frame].nil?
|
1834
|
+
if sub[:frame].respond_to?(:frameWidth)
|
1835
|
+
sub[:frame] = nil if sub[:type] == "Skylight" && v < 321
|
1836
|
+
sub[:frame] = nil if sub[:type] == "Door"
|
1837
|
+
sub[:frame] = nil if sub[:type] == "OverheadDoor"
|
1838
|
+
sub[:frame] = nil if sub[:type] == "TubularDaylightDome"
|
1839
|
+
sub[:frame] = nil if sub[:type] == "TubularDaylightDiffuser"
|
1840
|
+
log(WRN, "Skip '#{id}' FrameDivider (#{mth})") if sub[:frame].nil?
|
1841
|
+
else
|
1842
|
+
sub[:frame] = nil
|
1843
|
+
log(WRN, "Skip '#{id}' invalid FrameDivider object (#{mth})")
|
1844
|
+
end
|
1845
|
+
end
|
1846
|
+
|
1847
|
+
# The (optional) "assembly" must reference a valid OpenStudio
|
1848
|
+
# construction base, to explicitly assign to each instantiated sub
|
1849
|
+
# surface. If invalid, log/reset/ignore. Additional checks are later
|
1850
|
+
# activated once a sub surface is actually instantiated.
|
1851
|
+
unless sub[:assembly].nil?
|
1852
|
+
unless sub[:assembly].respond_to?(:isFenestration)
|
1853
|
+
log(WRN, "Skip invalid '#{id}' construction (#{mth})")
|
1854
|
+
sub[:assembly] = nil
|
1855
|
+
end
|
1856
|
+
end
|
1857
|
+
|
1858
|
+
# Log/reset negative numerical values. Set ~0 values to 0.
|
1859
|
+
sub.each do |key, value|
|
1860
|
+
next if key == :id
|
1861
|
+
next if key == :type
|
1862
|
+
next if key == :frame
|
1863
|
+
next if key == :assembly
|
1864
|
+
|
1865
|
+
return mismatch(key, value, cl5, mth, DBG, no) unless value.is_a?(cl5)
|
1866
|
+
next if key == :centreline
|
1867
|
+
|
1868
|
+
negative(key, mth, WRN) if value < 0
|
1869
|
+
value = 0.0 if value.abs < TOL
|
1870
|
+
end
|
1871
|
+
end
|
1872
|
+
|
1873
|
+
# --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
|
1874
|
+
# Log/reset (or abandon) conflicting user-set geometry key:value pairs:
|
1875
|
+
# :head e.g. std 80" door + frame/buffers (+ m)
|
1876
|
+
# :sill e.g. std 30" sill + frame/buffers (+ m)
|
1877
|
+
# :height any sub surface height, below "head" (+ m)
|
1878
|
+
# :width e.g. 1.200 m
|
1879
|
+
# :offset if array (+ m)
|
1880
|
+
# :centreline left or right of base surface centreline (+/- m)
|
1881
|
+
# :r_buffer buffer between sub/array and right-side corner (+ m)
|
1882
|
+
# :l_buffer buffer between sub/array and left-side corner (+ m)
|
1883
|
+
#
|
1884
|
+
# If successful, this will generate sub surfaces and add them to the model.
|
1885
|
+
subs.each do |sub|
|
1886
|
+
# Set-up unique sub parameters:
|
1887
|
+
# - Frame & Divider "width"
|
1888
|
+
# - minimum "clear glazing" limits
|
1889
|
+
# - buffers, etc.
|
1890
|
+
id = sub[:id]
|
1891
|
+
frame = 0
|
1892
|
+
frame = sub[:frame].frameWidth unless sub[:frame].nil?
|
1893
|
+
frames = 2 * frame
|
1894
|
+
buffer = frame + bfr
|
1895
|
+
buffers = 2 * buffer
|
1896
|
+
dim = 0.200 unless (3 * frame) > 0.200
|
1897
|
+
dim = 3 * frame if (3 * frame) > 0.200
|
1898
|
+
glass = dim - frames
|
1899
|
+
min_sill = buffer
|
1900
|
+
min_head = buffers + glass
|
1901
|
+
max_head = max_y - buffer
|
1902
|
+
max_sill = max_head - (buffers + glass)
|
1903
|
+
min_ljamb = buffer
|
1904
|
+
max_ljamb = max_x - (buffers + glass)
|
1905
|
+
min_rjamb = buffers + glass
|
1906
|
+
max_rjamb = max_x - buffer
|
1907
|
+
max_height = max_y - buffers
|
1908
|
+
max_width = max_x - buffers
|
1909
|
+
|
1910
|
+
# Default sub surface "head" & "sill" height (unless user-specified).
|
1911
|
+
typ_head = HEAD # standard 80" door
|
1912
|
+
typ_sill = SILL # standard 30" window sill
|
1913
|
+
|
1914
|
+
if sub.key?(:ratio)
|
1915
|
+
typ_head = mid_y * (1 + sub[:ratio]) if sub[:ratio] > 0.75
|
1916
|
+
typ_head = mid_y * (1 + sub[:ratio]) unless stype.downcase == "wall"
|
1917
|
+
typ_sill = mid_y * (1 - sub[:ratio]) if sub[:ratio] > 0.75
|
1918
|
+
typ_sill = mid_y * (1 - sub[:ratio]) unless stype.downcase == "wall"
|
1919
|
+
end
|
1920
|
+
|
1921
|
+
# Log/reset "height" if beyond min/max.
|
1922
|
+
if sub.key?(:height)
|
1923
|
+
unless sub[:height].between?(glass, max_height)
|
1924
|
+
sub[:height] = glass if sub[:height] < glass
|
1925
|
+
sub[:height] = max_height if sub[:height] > max_height
|
1926
|
+
log(WRN, "Reset '#{id}' height to #{sub[:height]} m (#{mth})")
|
1927
|
+
end
|
1928
|
+
end
|
1929
|
+
|
1930
|
+
# Log/reset "head" height if beyond min/max.
|
1931
|
+
if sub.key?(:head)
|
1932
|
+
unless sub[:head].between?(min_head, max_head)
|
1933
|
+
sub[:head] = max_head if sub[:head] > max_head
|
1934
|
+
sub[:head] = min_head if sub[:head] < min_head
|
1935
|
+
log(WRN, "Reset '#{id}' head height to #{sub[:head]} m (#{mth})")
|
1936
|
+
end
|
1937
|
+
end
|
1938
|
+
|
1939
|
+
# Log/reset "sill" height if beyond min/max.
|
1940
|
+
if sub.key?(:sill)
|
1941
|
+
unless sub[:sill].between?(min_sill, max_sill)
|
1942
|
+
sub[:sill] = max_sill if sub[:sill] > max_sill
|
1943
|
+
sub[:sill] = min_sill if sub[:sill] < min_sill
|
1944
|
+
log(WRN, "Reset '#{id}' sill height to #{sub[:sill]} m (#{mth})")
|
1945
|
+
end
|
1946
|
+
end
|
1947
|
+
|
1948
|
+
# At this point, "head", "sill" and/or "height" have been tentatively
|
1949
|
+
# validated (and/or have been corrected) independently from one another.
|
1950
|
+
# Log/reset "head" & "sill" heights if conflicting.
|
1951
|
+
if sub.key?(:head) && sub.key?(:sill) && sub[:head] < sub[:sill] + glass
|
1952
|
+
sill = sub[:head] - glass
|
1953
|
+
|
1954
|
+
if sill < min_sill
|
1955
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
1956
|
+
sub[:count ] = 0
|
1957
|
+
sub[:multiplier] = 0
|
1958
|
+
sub[:height ] = 0 if sub.key?(:height)
|
1959
|
+
sub[:width ] = 0 if sub.key?(:width)
|
1960
|
+
log(ERR, "Skip: invalid '#{id}' head/sill combo (#{mth})")
|
1961
|
+
next
|
1962
|
+
else
|
1963
|
+
sub[:sill] = sill
|
1964
|
+
log(WRN, "(Re)set '#{id}' sill height to #{sub[:sill]} m (#{mth})")
|
1965
|
+
end
|
1966
|
+
end
|
1967
|
+
|
1968
|
+
# Attempt to reconcile "head", "sill" and/or "height". If successful,
|
1969
|
+
# all 3x parameters are set (if missing), or reset if invalid.
|
1970
|
+
if sub.key?(:head) && sub.key?(:sill)
|
1971
|
+
height = sub[:head] - sub[:sill]
|
1972
|
+
|
1973
|
+
if sub.key?(:height) && (sub[:height] - height).abs > TOL
|
1974
|
+
log(WRN, "(Re)set '#{id}' height to #{height} m (#{mth})")
|
1975
|
+
end
|
1976
|
+
|
1977
|
+
sub[:height] = height
|
1978
|
+
elsif sub.key?(:head) # no "sill"
|
1979
|
+
if sub.key?(:height)
|
1980
|
+
sill = sub[:head] - sub[:height]
|
1981
|
+
|
1982
|
+
if sill < min_sill
|
1983
|
+
sill = min_sill
|
1984
|
+
height = sub[:head] - sill
|
1985
|
+
|
1986
|
+
if height < glass
|
1987
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
1988
|
+
sub[:count ] = 0
|
1989
|
+
sub[:multiplier] = 0
|
1990
|
+
sub[:height ] = 0 if sub.key?(:height)
|
1991
|
+
sub[:width ] = 0 if sub.key?(:width)
|
1992
|
+
log(ERR, "Skip: invalid '#{id}' head/height combo (#{mth})")
|
1993
|
+
next
|
1994
|
+
else
|
1995
|
+
sub[:sill ] = sill
|
1996
|
+
sub[:height] = height
|
1997
|
+
log(WRN, "(Re)set '#{id}' height to #{sub[:height]} m (#{mth})")
|
1998
|
+
end
|
1999
|
+
else
|
2000
|
+
sub[:sill] = sill
|
2001
|
+
end
|
2002
|
+
else
|
2003
|
+
sub[:sill ] = typ_sill
|
2004
|
+
sub[:height] = sub[:head] - sub[:sill]
|
2005
|
+
end
|
2006
|
+
elsif sub.key?(:sill) # no "head"
|
2007
|
+
if sub.key?(:height)
|
2008
|
+
head = sub[:sill] + sub[:height]
|
2009
|
+
|
2010
|
+
if head > max_head
|
2011
|
+
head = max_head
|
2012
|
+
height = head - sub[:sill]
|
2013
|
+
|
2014
|
+
if height < glass
|
2015
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
2016
|
+
sub[:count ] = 0
|
2017
|
+
sub[:multiplier] = 0
|
2018
|
+
sub[:height ] = 0 if sub.key?(:height)
|
2019
|
+
sub[:width ] = 0 if sub.key?(:width)
|
2020
|
+
log(ERR, "Skip: invalid '#{id}' sill/height combo (#{mth})")
|
2021
|
+
next
|
2022
|
+
else
|
2023
|
+
sub[:head ] = head
|
2024
|
+
sub[:height] = height
|
2025
|
+
log(WRN, "(Re)set '#{id}' height to #{sub[:height]} m (#{mth})")
|
2026
|
+
end
|
2027
|
+
else
|
2028
|
+
sub[:head] = head
|
2029
|
+
end
|
2030
|
+
else
|
2031
|
+
sub[:head ] = typ_head
|
2032
|
+
sub[:height] = sub[:head] - sub[:sill]
|
2033
|
+
end
|
2034
|
+
elsif sub.key?(:height) # neither "head" nor "sill"
|
2035
|
+
head = typ_head
|
2036
|
+
sill = head - sub[:height]
|
2037
|
+
|
2038
|
+
if sill < min_sill
|
2039
|
+
sill = min_sill
|
2040
|
+
head = sill + sub[:height]
|
2041
|
+
end
|
2042
|
+
|
2043
|
+
sub[:head] = head
|
2044
|
+
sub[:sill] = sill
|
2045
|
+
else
|
2046
|
+
sub[:head ] = typ_head
|
2047
|
+
sub[:sill ] = typ_sill
|
2048
|
+
sub[:height] = sub[:head] - sub[:sill]
|
2049
|
+
end
|
2050
|
+
|
2051
|
+
# Log/reset "width" if beyond min/max.
|
2052
|
+
if sub.key?(:width)
|
2053
|
+
unless sub[:width].between?(glass, max_width)
|
2054
|
+
sub[:width] = glass if sub[:width] < glass
|
2055
|
+
sub[:width] = max_width if sub[:width] > max_width
|
2056
|
+
log(WRN, "Reset '#{id}' width to #{sub[:width]} m (#{mth})")
|
2057
|
+
end
|
2058
|
+
end
|
2059
|
+
|
2060
|
+
# Log/reset "count" if < 1.
|
2061
|
+
if sub.key?(:count)
|
2062
|
+
if sub[:count] < 1
|
2063
|
+
sub[:count] = 1
|
2064
|
+
log(WRN, "Reset '#{id}' count to #{sub[:count]} (#{mth})")
|
2065
|
+
end
|
2066
|
+
end
|
2067
|
+
|
2068
|
+
sub[:count] = 1 unless sub.key?(:count)
|
2069
|
+
|
2070
|
+
# Log/reset if left-sided buffer under min jamb position.
|
2071
|
+
if sub.key?(:l_buffer)
|
2072
|
+
if sub[:l_buffer] < min_ljamb
|
2073
|
+
sub[:l_buffer] = min_ljamb
|
2074
|
+
log(WRN, "Reset '#{id}' left buffer to #{sub[:l_buffer]} m (#{mth})")
|
2075
|
+
end
|
2076
|
+
end
|
2077
|
+
|
2078
|
+
# Log/reset if right-sided buffer beyond max jamb position.
|
2079
|
+
if sub.key?(:r_buffer)
|
2080
|
+
if sub[:r_buffer] > max_rjamb
|
2081
|
+
sub[:r_buffer] = min_rjamb
|
2082
|
+
log(WRN, "Reset '#{id}' right buffer to #{sub[:r_buffer]} m (#{mth})")
|
2083
|
+
end
|
2084
|
+
end
|
2085
|
+
|
2086
|
+
centre = mid_x
|
2087
|
+
centre += sub[:centreline] if sub.key?(:centreline)
|
2088
|
+
n = sub[:count ]
|
2089
|
+
h = sub[:height ] + frames
|
2090
|
+
w = 0 # overall width of sub(s) bounding box (to calculate)
|
2091
|
+
x0 = 0 # left-side X-axis coordinate of sub(s) bounding box
|
2092
|
+
xf = 0 # right-side X-axis coordinate of sub(s) bounding box
|
2093
|
+
|
2094
|
+
# Log/reset "offset", if conflicting vs "width".
|
2095
|
+
if sub.key?(:ratio)
|
2096
|
+
if sub[:ratio] < TOL
|
2097
|
+
sub[:ratio ] = 0
|
2098
|
+
sub[:count ] = 0
|
2099
|
+
sub[:multiplier] = 0
|
2100
|
+
sub[:height ] = 0 if sub.key?(:height)
|
2101
|
+
sub[:width ] = 0 if sub.key?(:width)
|
2102
|
+
log(ERR, "Skip: '#{id}' ratio ~0 (#{mth})")
|
2103
|
+
next
|
2104
|
+
end
|
2105
|
+
|
2106
|
+
# Log/reset if "ratio" beyond min/max?
|
2107
|
+
unless sub[:ratio].between?(min, max)
|
2108
|
+
sub[:ratio] = min if sub[:ratio] < min
|
2109
|
+
sub[:ratio] = max if sub[:ratio] > max
|
2110
|
+
log(WRN, "Reset ratio (min/max) to #{sub[:ratio]} (#{mth})")
|
2111
|
+
end
|
2112
|
+
|
2113
|
+
# Log/reset "count" unless 1.
|
2114
|
+
unless sub[:count] == 1
|
2115
|
+
sub[:count] = 1
|
2116
|
+
log(WRN, "Reset count (ratio) to 1 (#{mth})")
|
2117
|
+
end
|
2118
|
+
|
2119
|
+
area = s.grossArea * sub[:ratio] # sub m2, including (optional) frames
|
2120
|
+
w = area / h
|
2121
|
+
width = w - frames
|
2122
|
+
x0 = centre - w/2
|
2123
|
+
xf = centre + w/2
|
2124
|
+
|
2125
|
+
if sub.key?(:l_buffer)
|
2126
|
+
if sub.key?(:centreline)
|
2127
|
+
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
2128
|
+
else
|
2129
|
+
x0 = sub[:l_buffer] - frame
|
2130
|
+
xf = x0 + w
|
2131
|
+
centre = x0 + w/2
|
2132
|
+
end
|
2133
|
+
elsif sub.key?(:r_buffer)
|
2134
|
+
if sub.key?(:centreline)
|
2135
|
+
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
2136
|
+
else
|
2137
|
+
xf = max_x - sub[:r_buffer] + frame
|
2138
|
+
x0 = xf - w
|
2139
|
+
centre = x0 + w/2
|
2140
|
+
end
|
2141
|
+
end
|
2142
|
+
|
2143
|
+
# Too wide?
|
2144
|
+
if x0 < min_ljamb || xf > max_rjamb
|
2145
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
2146
|
+
sub[:count ] = 0
|
2147
|
+
sub[:multiplier] = 0
|
2148
|
+
sub[:height ] = 0 if sub.key?(:height)
|
2149
|
+
sub[:width ] = 0 if sub.key?(:width)
|
2150
|
+
log(ERR, "Skip: invalid (ratio) width/centreline (#{mth})")
|
2151
|
+
next
|
2152
|
+
end
|
2153
|
+
|
2154
|
+
if sub.key?(:width) && (sub[:width] - width).abs > TOL
|
2155
|
+
sub[:width] = width
|
2156
|
+
log(WRN, "Reset width (ratio) to #{sub[:width]} (#{mth})")
|
2157
|
+
end
|
2158
|
+
|
2159
|
+
sub[:width] = width unless sub.key?(:width)
|
2160
|
+
else
|
2161
|
+
unless sub.key?(:width)
|
2162
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
2163
|
+
sub[:count ] = 0
|
2164
|
+
sub[:multiplier] = 0
|
2165
|
+
sub[:height ] = 0 if sub.key?(:height)
|
2166
|
+
sub[:width ] = 0 if sub.key?(:width)
|
2167
|
+
log(ERR, "Skip: missing '#{id}' width (#{mth})")
|
2168
|
+
next
|
2169
|
+
end
|
2170
|
+
|
2171
|
+
width = sub[:width] + frames
|
2172
|
+
gap = (max_x - n * width) / (n + 1)
|
2173
|
+
gap = sub[:offset] - width if sub.key?(:offset)
|
2174
|
+
gap = 0 if gap < bfr
|
2175
|
+
offset = gap + width
|
2176
|
+
|
2177
|
+
if sub.key?(:offset) && (offset - sub[:offset]).abs > TOL
|
2178
|
+
sub[:offset] = offset
|
2179
|
+
log(WRN, "Reset sub offset to #{sub[:offset]} m (#{mth})")
|
2180
|
+
end
|
2181
|
+
|
2182
|
+
sub[:offset] = offset unless sub.key?(:offset)
|
2183
|
+
|
2184
|
+
# Overall width (including frames) of bounding box around array.
|
2185
|
+
w = n * width + (n - 1) * gap
|
2186
|
+
x0 = centre - w/2
|
2187
|
+
xf = centre + w/2
|
2188
|
+
|
2189
|
+
if sub.key?(:l_buffer)
|
2190
|
+
if sub.key?(:centreline)
|
2191
|
+
log(WRN, "Skip #{id} left buffer (vs centreline) (#{mth})")
|
2192
|
+
else
|
2193
|
+
x0 = sub[:l_buffer] - frame
|
2194
|
+
xf = x0 + w
|
2195
|
+
centre = x0 + w/2
|
2196
|
+
end
|
2197
|
+
elsif sub.key?(:r_buffer)
|
2198
|
+
if sub.key?(:centreline)
|
2199
|
+
log(WRN, "Skip #{id} right buffer (vs centreline) (#{mth})")
|
2200
|
+
else
|
2201
|
+
xf = max_x - sub[:r_buffer] + frame
|
2202
|
+
x0 = xf - w
|
2203
|
+
centre = x0 + w/2
|
2204
|
+
end
|
2205
|
+
end
|
2206
|
+
|
2207
|
+
# Too wide?
|
2208
|
+
if x0 < bfr || xf > max_x - bfr
|
2209
|
+
sub[:ratio ] = 0 if sub.key?(:ratio)
|
2210
|
+
sub[:count ] = 0
|
2211
|
+
sub[:multiplier] = 0
|
2212
|
+
sub[:height ] = 0 if sub.key?(:height)
|
2213
|
+
sub[:width ] = 0 if sub.key?(:width)
|
2214
|
+
log(ERR, "Skip: invalid array width/centreline (#{mth})")
|
2215
|
+
next
|
2216
|
+
end
|
2217
|
+
end
|
2218
|
+
|
2219
|
+
# Initialize left-side X-axis coordinate of only/first sub.
|
2220
|
+
pos = x0 + frame
|
2221
|
+
|
2222
|
+
# Generate sub(s).
|
2223
|
+
sub[:count].times do |i|
|
2224
|
+
name = "#{id}:#{i}"
|
2225
|
+
fr = 0
|
2226
|
+
fr = sub[:frame].frameWidth if sub[:frame]
|
2227
|
+
|
2228
|
+
vec = OpenStudio::Point3dVector.new
|
2229
|
+
vec << OpenStudio::Point3d.new(pos, sub[:head], 0)
|
2230
|
+
vec << OpenStudio::Point3d.new(pos, sub[:sill], 0)
|
2231
|
+
vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:sill], 0)
|
2232
|
+
vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:head], 0)
|
2233
|
+
vec = tr * vec
|
2234
|
+
|
2235
|
+
# Log/skip if conflict between individual sub and base surface.
|
2236
|
+
vc = vec
|
2237
|
+
vc = offset(vc, fr, 300) if fr > 0
|
2238
|
+
ok = fits?(vc, s.vertices, name, nom)
|
2239
|
+
log(ERR, "Skip '#{name}': won't fit in '#{nom}' (#{mth})") unless ok
|
2240
|
+
break unless ok
|
2241
|
+
|
2242
|
+
# Log/skip if conflicts with existing subs (even if same array).
|
2243
|
+
s.subSurfaces.each do |sb|
|
2244
|
+
nome = sb.nameString
|
2245
|
+
fd = sb.windowPropertyFrameAndDivider
|
2246
|
+
fr = 0 if fd.empty?
|
2247
|
+
fr = fd.get.frameWidth unless fd.empty?
|
2248
|
+
vk = sb.vertices
|
2249
|
+
vk = offset(vk, fr, 300) if fr > 0
|
2250
|
+
oops = overlaps?(vc, vk, name, nome)
|
2251
|
+
log(ERR, "Skip '#{name}': overlaps '#{nome}' (#{mth})") if oops
|
2252
|
+
ok = false if oops
|
2253
|
+
break if oops
|
2254
|
+
end
|
2255
|
+
|
2256
|
+
break unless ok
|
2257
|
+
|
2258
|
+
sb = OpenStudio::Model::SubSurface.new(vec, model)
|
2259
|
+
sb.setName(name)
|
2260
|
+
sb.setSubSurfaceType(sub[:type])
|
2261
|
+
sb.setConstruction(sub[:assembly]) if sub[:assembly]
|
2262
|
+
ok = sb.allowWindowPropertyFrameAndDivider
|
2263
|
+
sb.setWindowPropertyFrameAndDivider(sub[:frame]) if sub[:frame] && ok
|
2264
|
+
sb.setMultiplier(sub[:multiplier]) if sub[:multiplier] > 1
|
2265
|
+
sb.setSurface(s)
|
2266
|
+
|
2267
|
+
# Reset "pos" if array.
|
2268
|
+
pos += sub[:offset] if sub.key?(:offset)
|
2269
|
+
end
|
2270
|
+
end
|
2271
|
+
|
2272
|
+
true
|
2273
|
+
end
|
2274
|
+
|
1629
2275
|
##
|
1630
2276
|
# Callback when other modules extend OSlg
|
1631
2277
|
#
|
data/lib/osut/version.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# BSD 3-Clause License
|
2
2
|
#
|
3
|
-
# Copyright (c) 2022, Denis Bourgeois
|
3
|
+
# Copyright (c) 2022-2023, Denis Bourgeois
|
4
4
|
# All rights reserved.
|
5
5
|
#
|
6
6
|
# Redistribution and use in source and binary forms, with or without
|
@@ -29,5 +29,5 @@
|
|
29
29
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
30
|
|
31
31
|
module OSut
|
32
|
-
VERSION = "0.
|
32
|
+
VERSION = "0.3.0".freeze
|
33
33
|
end
|
data/lib/osut.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: osut
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Denis Bourgeois
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oslg
|
@@ -90,7 +90,7 @@ licenses:
|
|
90
90
|
- BSD-3-Clause
|
91
91
|
metadata:
|
92
92
|
homepage_uri: https://github.com/rd2/osut
|
93
|
-
source_code_uri: https://github.com/rd2/osut/tree/v0.
|
93
|
+
source_code_uri: https://github.com/rd2/osut/tree/v0.3.0
|
94
94
|
bug_tracker_uri: https://github.com/rd2/osut/issues
|
95
95
|
post_install_message:
|
96
96
|
rdoc_options: []
|