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 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: []