osut 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|