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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bbbc6887949cfd0d1aed5c460d993196fa86d1a13f366f7f852e418aa9457a0
4
- data.tar.gz: b38eaa81d84a79655adb10a06bb1fa299ffff60051d187f2b36bad38fa4d78e0
3
+ metadata.gz: 2491bf7ed2ed4277ccfe4087a661660e720bd7f417b1e51f3c1f81715e53193f
4
+ data.tar.gz: 7c07191bf8cb725fdb9a04e15269aa6431fd7aa66955b9b99b2c1d61929033c6
5
5
  SHA512:
6
- metadata.gz: 1eb2cd49987ce2a4bf7a6a9fe0ed7048bc78dce644aa88e68173861ce4e52fc2bfe59bb3c4a7b353426a1bfa30f45af102ca3e509ac91945b82673545f1a4d4d
7
- data.tar.gz: bb8075d22d660b3f177716463baf3bc2b0e788269dc05f60086272dc5fec193b93a207f94500fa26cf8d786fdcd3810c0d3defb50eca4c39b3a05cf7c8d2412c
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-18.04
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-18.04
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-20.04
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-20.04
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
@@ -8,4 +8,6 @@ _yardoc
8
8
  doc/
9
9
  rdoc
10
10
 
11
+ spec/files/osms/out/
12
+
11
13
  .DS_Store
data/LICENSE 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
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., >= 50 W/m2 in Climate Zone CZ-7
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., heating and/or cooling based, no distinction for
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 (OSM) in an incomplete or preliminary state, e.g.
113
- # holding fully-formed ENCLOSED spaces without thermal zoning information or
114
- # setpoint temperatures (early design stage assessments of form, porosity or
115
- # envelope), all OSM spaces will be considered CONDITIONED, presuming
116
- # setpoints of ~21°C (heating) and ~24°C (cooling).
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 OSM,
119
- # spaces/zones WITHOUT valid heating or cooling setpoints are considered as
120
- # UNCONDITIONED or UNENCLOSED spaces (like attics), or INDIRECTLY CONDITIONED
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 = "OSut::#{__callee__}"
252
- cl = OpenStudio::Model::ScheduleInterval
253
- vals = []
254
- prev_str = ""
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 = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
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
- unless type.standardsSpaceType.empty?
719
- type = type.standardsSpaceType.get
720
- return type.downcase == "plenum" # C
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 ground construction.
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
- t += 273.0 # °C to K
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", v.x, cl2, mth, DBG, v) unless v.x.respond_to?(:to_f)
1248
- return mismatch("y", v.y, cl2, mth, DBG, v) unless v.y.respond_to?(:to_f)
1249
- return mismatch("z", v.z, cl2, mth, DBG, v) unless v.z.respond_to?(:to_f)
1250
- return mismatch("m", m, cl2, mth, DBG, v) unless m.respond_to?(:to_f)
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 = "TBD::#{__callee__}"
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.2.7".freeze
32
+ VERSION = "0.3.0".freeze
33
33
  end
data/lib/osut.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
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.2.7
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: 2022-08-21 00:00:00.000000000 Z
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.2.7
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: []