tbd 3.1.1 → 3.2.1

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.
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
  #
3
- # Copyright (c) 2020-2022 Denis Bourgeois & Dan Macumber
3
+ # Copyright (c) 2020-2023 Denis Bourgeois & Dan Macumber
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -36,187 +36,217 @@ class TBDTest < Minitest::Test
36
36
  # end
37
37
 
38
38
  def test_number_of_arguments_and_argument_names
39
- # create an instance of the measure
40
39
  measure = TBDMeasure.new
41
- # make an empty model
42
40
  model = OpenStudio::Model::Model.new
43
-
44
- # get arguments and test that they are what we are expecting
45
41
  arguments = measure.arguments(model)
46
- assert_equal(14, arguments.size)
42
+ assert_equal(15, arguments.size)
47
43
  end
48
44
 
49
45
  def test_no_load_tbd_json
50
- # create an instance of the measure
51
46
  measure = TBDMeasure.new
52
47
 
53
- # Output dirs
54
- seed_dir = File.join(__dir__, 'output/no_load_tbd_json/')
48
+ # Output directories.
49
+ seed_dir = File.join(__dir__, "output/no_load_tbd_json/")
55
50
  FileUtils.mkdir_p(seed_dir)
56
- seed_path = File.join(seed_dir, 'in.osm')
51
+ seed_path = File.join(seed_dir, "in.osm")
57
52
 
58
- # create runner with empty OSW
53
+ # Create runner with empty OSW, and example test model.
59
54
  osw = OpenStudio::WorkflowJSON.new
60
55
  osw.setSeedFile(seed_path)
61
56
  runner = OpenStudio::Measure::OSRunner.new(osw)
62
-
63
- # create example test model
64
57
  model = OpenStudio::Model::exampleModel
65
58
  model.save(seed_path, true)
66
59
 
67
- # get arguments
60
+ # Get measure arguments.
68
61
  arguments = measure.arguments(model)
69
62
  argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)
70
63
 
71
- # Create hash of argument values. If the argument has a default that you
72
- # want to use, you don't need it in the hash.
73
- args_hash = {}
74
- args_hash["option" ] = "efficient (BETBG)"
75
- args_hash["write_tbd_json"] = true
76
- args_hash["gen_UA_report" ] = true
77
- args_hash["wall_option" ] = "ALL wall constructions"
78
- args_hash["wall_ut" ] = 0.5
79
- # using defaults values from measure.rb for other arguments
80
-
81
- # populate argument with specified hash value if specified
64
+ # Hash of argument values (defaults from measure.rb for other arguments).
65
+ argh = {}
66
+ argh["option" ] = "efficient (BETBG)"
67
+ argh["write_tbd_json"] = true
68
+ argh["gen_UA_report" ] = true
69
+ argh["wall_option" ] = "ALL wall constructions"
70
+ argh["wall_ut" ] = 0.5
71
+
72
+ # Populate arguments with specified hash value if specified.
82
73
  arguments.each do |arg|
83
74
  temp_arg_var = arg.clone
84
- gotit = args_hash.key?(arg.name)
85
- assert(temp_arg_var.setValue(args_hash[arg.name])) if gotit
75
+ assert(temp_arg_var.setValue(argh[arg.name])) if argh.key?(arg.name)
86
76
  argument_map[arg.name] = temp_arg_var
87
77
  end
88
78
 
89
- # run the measure
79
+ # Run the measure and assert that it ran correctly.
90
80
  Dir.chdir(seed_dir)
91
81
  measure.run(model, runner, argument_map)
92
82
  result = runner.result
93
-
94
- # show the output
95
83
  show_output(result)
96
-
97
- # assert that it ran correctly
98
- assert_equal('Success', result.value.valueName)
84
+ assert_equal("Success", result.value.valueName)
99
85
  assert(result.warnings.empty?)
86
+ assert(result.errors.empty?)
100
87
 
101
- # save the model to test output directory
102
- #output_file_path = "#{File.dirname(__FILE__)}//output/test_output.osm"
103
- #model.save(output_file_path, true)
88
+ # Save the model to test output directory.
89
+ output_path = File.join(seed_dir, "out.osm")
90
+ model.save(output_path, true)
104
91
  end
105
92
 
106
93
  def test_load_tbd_json
107
- # create an instance of the measure
108
94
  measure = TBDMeasure.new
109
95
 
110
- # Output dirs
96
+ # Output directories.
111
97
  seed_dir = File.join(__dir__, "output/load_tbd_json/")
112
98
  FileUtils.mkdir_p(seed_dir)
113
99
  seed_path = File.join(seed_dir, "in.osm")
114
100
 
115
- # create runner with empty OSW
101
+ # Create runner with empty OSW, and example test model.
116
102
  osw = OpenStudio::WorkflowJSON.new
117
103
  osw.setSeedFile(seed_path)
118
104
  runner = OpenStudio::Measure::OSRunner.new(osw)
119
-
120
- # create example test model
121
105
  model = OpenStudio::Model::exampleModel
122
106
  model.save(seed_path, true)
123
107
 
124
- # copy tdb.json next to seed
108
+ # Copy tdb.json next to seed.
125
109
  origin_pth = File.join(__dir__, "tbd_full_PSI.json")
126
110
  target_pth = File.join(seed_dir, "tbd.json")
127
111
  FileUtils.cp(origin_pth, target_pth)
128
112
 
129
- # get arguments
113
+ # Get measure arguments.
130
114
  arguments = measure.arguments(model)
131
115
  argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)
132
116
 
133
- # Create hash of argument values. If the argument has a default that you
134
- # want to use, you don't need it in the hash.
135
- args_hash = {"load_tbd_json" => true}
136
- # using defaults values from measure.rb for other arguments
117
+ # Hash of argument values (defaults from measure.rb for other arguments).
118
+ argh = {}
119
+ argh["load_tbd_json" ] = true
137
120
 
138
- # populate argument with specified hash value if specified
121
+ # Populate arguments with specified hash value if specified.
139
122
  arguments.each do |arg|
140
123
  temp_arg_var = arg.clone
141
- if args_hash.key?(arg.name)
142
- assert(temp_arg_var.setValue(args_hash[arg.name]))
143
- end
124
+ assert(temp_arg_var.setValue(argh[arg.name])) if argh.key?(arg.name)
144
125
  argument_map[arg.name] = temp_arg_var
145
126
  end
146
127
 
147
- # run the measure
128
+ # Run the measure and assert that it ran correctly.
148
129
  Dir.chdir(seed_dir)
149
130
  measure.run(model, runner, argument_map)
150
131
  result = runner.result
151
-
152
- # show the output
153
132
  show_output(result)
154
-
155
- # assert that it ran correctly
156
- assert_equal('Success', result.value.valueName)
133
+ assert_equal("Success", result.value.valueName)
157
134
  assert(result.warnings.empty?)
135
+ assert(result.errors.empty?)
158
136
 
159
- # save the model to test output directory
160
- #output_file_path = "#{File.dirname(__FILE__)}//output/test_output.osm"
161
- #model.save(output_file_path, true)
137
+ # Save the model to test output directory.
138
+ output_path = File.join(seed_dir, "out.osm")
139
+ model.save(output_path, true)
162
140
  end
163
141
 
164
142
  def test_load_tbd_json_error
165
- # create an instance of the measure
166
143
  measure = TBDMeasure.new
167
144
 
168
- # Output dirs
145
+ # Output directories.
169
146
  seed_dir = File.join(__dir__, "output/load_tbd_json_error/")
170
147
  FileUtils.mkdir_p(seed_dir)
171
148
  seed_path = File.join(seed_dir, "in.osm")
172
149
 
173
- # create runner with empty OSW
150
+ # Create runner with empty OSW, and example test model.
174
151
  osw = OpenStudio::WorkflowJSON.new
175
152
  osw.setSeedFile(seed_path)
176
153
  runner = OpenStudio::Measure::OSRunner.new(osw)
177
-
178
- # create example test model
179
154
  model = OpenStudio::Model::exampleModel
180
155
  model.save(seed_path, true)
181
156
 
182
- # do not copy tdb.json next to seed
157
+ # POSTULATED USER ERROR: Do not copy tdb.json next to seed.
183
158
 
184
- # get arguments
159
+ # Get measure arguments.
185
160
  arguments = measure.arguments(model)
186
161
  argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)
187
162
 
188
- # Create hash of argument values. If the argument has a default that you
189
- # want to use, you don't need it in the hash.
190
- args_hash = {"load_tbd_json" => true}
191
- # using defaults values from measure.rb for other arguments
163
+ # Hash of argument values (defaults from measure.rb for other arguments).
164
+ argh = {}
165
+ argh["load_tbd_json" ] = true
192
166
 
193
- # populate argument with specified hash value if specified
167
+ # Populate argument with specified hash value if specified.
194
168
  arguments.each do |arg|
195
169
  temp_arg_var = arg.clone
196
- if args_hash.key?(arg.name)
197
- assert(temp_arg_var.setValue(args_hash[arg.name]))
198
- end
170
+ assert(temp_arg_var.setValue(argh[arg.name])) if argh.key?(arg.name)
199
171
  argument_map[arg.name] = temp_arg_var
200
172
  end
201
173
 
202
- # run the measure
174
+ # Run the measure, assert that it did not run correctly.
203
175
  Dir.chdir(seed_dir)
204
176
  measure.run(model, runner, argument_map)
205
177
  result = runner.result
206
-
207
- # show the output
208
178
  show_output(result)
179
+ assert_equal("Fail", result.value.valueName)
209
180
 
210
- # assert that it ran correctly
211
- assert_equal('Fail', result.value.valueName)
212
- assert(result.errors.size == 1)
213
181
  assert(result.warnings.size == 1)
214
- puts result.warnings[0].logMessage
215
- log_message = "Can't find 'tbd.json' - simulation halted"
216
- assert(result.warnings[0].logMessage == log_message)
182
+ message = result.warnings[0].logMessage
183
+ puts message
184
+ assert(message.include?("Can't find 'tbd.json' - simulation halted"))
185
+
186
+ assert(result.errors.size == 1)
187
+ message = result.errors[0].logMessage
188
+ puts message
189
+ assert(message.include?("Halting all TBD processes, "))
190
+ assert(message.include?("and halting OpenStudio - see 'tbd.out.json'"))
191
+
192
+ # Save the model to test output directory.
193
+ output_path = File.join(seed_dir, "out.osm")
194
+ model.save(output_path, true)
195
+ end
196
+
197
+ def test_tbd_kiva_massless_error
198
+ measure = TBDMeasure.new
199
+
200
+ # Output directories.
201
+ seed_dir = File.join(__dir__, "output/tbd_kiva_massless_error/")
202
+ FileUtils.mkdir_p(seed_dir)
203
+ seed_path = File.join(seed_dir, "in.osm")
204
+
205
+ # Create runner with empty OSW, and example test model.
206
+ osw = OpenStudio::WorkflowJSON.new
207
+ osw.setSeedFile(seed_path)
208
+ runner = OpenStudio::Measure::OSRunner.new(osw)
209
+ model = OpenStudio::Model::exampleModel
210
+ model.save(seed_path, true)
211
+
212
+ # Copy tdb.json next to seed.
213
+ origin_pth = File.join(__dir__, "tbd_full_PSI.json")
214
+ target_pth = File.join(seed_dir, "tbd.json")
215
+ FileUtils.cp(origin_pth, target_pth)
216
+
217
+ # Get measure arguments.
218
+ arguments = measure.arguments(model)
219
+ argument_map = OpenStudio::Measure.convertOSArgumentVectorToMap(arguments)
220
+
221
+ # Hash of argument values (defaults from measure.rb for other arguments).
222
+ argh = {}
223
+ argh["gen_kiva_force"] = true
224
+
225
+ # POSTULATED USER ERROR : Slab on grade construction holds a massless layer.
226
+
227
+ # Populate argument with specified hash value if specified.
228
+ arguments.each do |arg|
229
+ temp_arg_var = arg.clone
230
+ assert(temp_arg_var.setValue(argh[arg.name])) if argh.key?(arg.name)
231
+ argument_map[arg.name] = temp_arg_var
232
+ end
233
+
234
+ # Run the measure, assert that it did not run correctly.
235
+ Dir.chdir(seed_dir)
236
+ measure.run(model, runner, argument_map)
237
+ result = runner.result
238
+ show_output(result)
239
+ assert_equal("Fail", result.value.valueName)
240
+ assert(result.warnings.empty?)
241
+ assert(result.errors.size == 4)
242
+
243
+ result.errors.each do |error|
244
+ assert(error.logMessage.include?("KIVA requires standard materials ("))
245
+ end
217
246
 
218
- # save the model to test output directory
219
- #output_file_path = "#{File.dirname(__FILE__)}//output/test_output.osm"
220
- #model.save(output_file_path, true)
247
+ # Save the model to test output directory. There should be neither instance
248
+ # of KIVA objects nor TBD derated materials/constructions.
249
+ output_path = File.join(seed_dir, "out.osm")
250
+ model.save(output_path, true)
221
251
  end
222
252
  end
data/lib/tbd/geo.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
  #
3
- # Copyright (c) 2020-2022 Denis Bourgeois & Dan Macumber
3
+ # Copyright (c) 2020-2023 Denis Bourgeois & Dan Macumber
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -22,24 +22,27 @@
22
22
 
23
23
  module TBD
24
24
  ##
25
- # Check for matching Topolys vertex pairs between edges (within TOL).
25
+ # Check for matching Topolys vertex pairs between edges.
26
26
  #
27
27
  # @param e1 [Hash] first edge
28
28
  # @param e2 [Hash] second edge
29
+ # @param tol [Float] user-set tolerance (> TOL) in m
29
30
  #
30
31
  # @return [Bool] true if edges share vertex pairs
31
32
  # @return [Bool] false if invalid input
32
- def matches?(e1 = {}, e2 = {})
33
+ def matches?(e1 = {}, e2 = {}, tol = TOL)
33
34
  mth = "TBD::#{__callee__}"
34
35
  cl = Topolys::Point3D
35
36
  a = false
36
37
 
37
38
  return mismatch("e1", e1, Hash, mth, DBG, a) unless e1.is_a?(Hash)
38
39
  return mismatch("e2", e2, Hash, mth, DBG, a) unless e2.is_a?(Hash)
40
+
39
41
  return hashkey("e1", e1, :v0, mth, DBG, a) unless e1.key?(:v0)
40
42
  return hashkey("e1", e1, :v1, mth, DBG, a) unless e1.key?(:v1)
41
43
  return hashkey("e2", e2, :v0, mth, DBG, a) unless e2.key?(:v0)
42
44
  return hashkey("e2", e2, :v1, mth, DBG, a) unless e2.key?(:v1)
45
+
43
46
  return mismatch("e1 :v0", e1[:v0], cl, mth, DBG, a) unless e1[:v0].is_a?(cl)
44
47
  return mismatch("e1 :v1", e1[:v1], cl, mth, DBG, a) unless e1[:v1].is_a?(cl)
45
48
  return mismatch("e2 :v0", e2[:v0], cl, mth, DBG, a) unless e2[:v0].is_a?(cl)
@@ -51,26 +54,29 @@ module TBD
51
54
  return zero("e1", mth, DBG, a) if e1_vector.magnitude < TOL
52
55
  return zero("e2", mth, DBG, a) if e2_vector.magnitude < TOL
53
56
 
57
+ return mismatch("e1", e1, Hash, mth, DBG, a) unless tol.is_a?(Numeric)
58
+ return zero("tol", mth, DBG, a) if tol < TOL
59
+
54
60
  return true if
55
61
  (
56
62
  (
57
- ( (e1[:v0].x - e2[:v0].x).abs < TOL &&
58
- (e1[:v0].y - e2[:v0].y).abs < TOL &&
59
- (e1[:v0].z - e2[:v0].z).abs < TOL
63
+ ( (e1[:v0].x - e2[:v0].x).abs < tol &&
64
+ (e1[:v0].y - e2[:v0].y).abs < tol &&
65
+ (e1[:v0].z - e2[:v0].z).abs < tol
60
66
  ) ||
61
- ( (e1[:v0].x - e2[:v1].x).abs < TOL &&
62
- (e1[:v0].y - e2[:v1].y).abs < TOL &&
63
- (e1[:v0].z - e2[:v1].z).abs < TOL
67
+ ( (e1[:v0].x - e2[:v1].x).abs < tol &&
68
+ (e1[:v0].y - e2[:v1].y).abs < tol &&
69
+ (e1[:v0].z - e2[:v1].z).abs < tol
64
70
  )
65
71
  ) &&
66
72
  (
67
- ( (e1[:v1].x - e2[:v0].x).abs < TOL &&
68
- (e1[:v1].y - e2[:v0].y).abs < TOL &&
69
- (e1[:v1].z - e2[:v0].z).abs < TOL
73
+ ( (e1[:v1].x - e2[:v0].x).abs < tol &&
74
+ (e1[:v1].y - e2[:v0].y).abs < tol &&
75
+ (e1[:v1].z - e2[:v0].z).abs < tol
70
76
  ) ||
71
- ( (e1[:v1].x - e2[:v1].x).abs < TOL &&
72
- (e1[:v1].y - e2[:v1].y).abs < TOL &&
73
- (e1[:v1].z - e2[:v1].z).abs < TOL
77
+ ( (e1[:v1].x - e2[:v1].x).abs < tol &&
78
+ (e1[:v1].y - e2[:v1].y).abs < tol &&
79
+ (e1[:v1].z - e2[:v1].z).abs < tol
74
80
  )
75
81
  )
76
82
  )
@@ -354,6 +360,7 @@ module TBD
354
360
  next unless valid
355
361
  vec = s.vertices
356
362
  area = s.grossArea
363
+ mult = s.multiplier
357
364
  typ = s.subSurfaceType.downcase
358
365
  type = :skylight
359
366
  type = :window if typ.include?("window" )
@@ -442,6 +449,7 @@ module TBD
442
449
  n: n,
443
450
  gross: s.grossArea,
444
451
  area: area,
452
+ mult: mult,
445
453
  type: type,
446
454
  u: u,
447
455
  unhinged: unhinged }
@@ -478,7 +486,9 @@ module TBD
478
486
  end
479
487
 
480
488
  subarea = 0
481
- subs.values.each { |sub| subarea += sub[:area] }
489
+
490
+ subs.values.each { |sub| subarea += sub[:area] * sub[:mult] }
491
+
482
492
  surf[:net] = surf[:gross] - subarea
483
493
 
484
494
  # Tranform final Point 3D sets, and store.
@@ -604,6 +614,36 @@ module TBD
604
614
  return mismatch("floors", floors, cl2, mth, DBG, a) unless floors.is_a?(cl2)
605
615
  return mismatch("edges", edges, cl2, mth, DBG, a) unless edges.is_a?(cl2)
606
616
 
617
+ kva = true
618
+
619
+ # Pre-validate foundation-facing constructions.
620
+ model.getSurfaces.each do |s|
621
+ id = s.nameString
622
+ construction = s.construction
623
+ next unless s.outsideBoundaryCondition.downcase == "foundation"
624
+
625
+ if construction.empty?
626
+ log(ERR, "Invalid construction for KIVA (see #{id})")
627
+ kva = false if kva
628
+ else
629
+ construction = construction.get.to_LayeredConstruction
630
+
631
+ if construction.empty?
632
+ log(ERR, "KIVA requires layered constructions (see #{id})")
633
+ kva = false if kva
634
+ else
635
+ construction = construction.get
636
+
637
+ unless standardOpaqueLayers?(construction)
638
+ log(ERR, "KIVA requires standard materials (see #{id})")
639
+ kva = false if kva
640
+ end
641
+ end
642
+ end
643
+ end
644
+
645
+ return a unless kva
646
+
607
647
  # Strictly relying on Kiva's total exposed perimeter approach.
608
648
  arg = "TotalExposedPerimeter"
609
649
  kiva = true
data/lib/tbd/psi.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
  #
3
- # Copyright (c) 2020-2022 Denis Bourgeois & Dan Macumber
3
+ # Copyright (c) 2020-2023 Denis Bourgeois & Dan Macumber
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  # of this software and associated documentation files (the "Software"), to deal
@@ -913,6 +913,7 @@ module TBD
913
913
  return mismatch("argh", argh, Hash, mth, DBG, tbd) unless argh.is_a?(Hash)
914
914
 
915
915
  argh = {} if argh.empty?
916
+ argh[:sub_tol ] = TBD::TOL unless argh.key?(:sub_tol )
916
917
  argh[:option ] = "" unless argh.key?(:option )
917
918
  argh[:io_path ] = nil unless argh.key?(:io_path )
918
919
  argh[:schema_path ] = nil unless argh.key?(:schema_path )
@@ -974,8 +975,9 @@ module TBD
974
975
  end
975
976
  end
976
977
 
977
- surface[:heating] = heat[:spt] if heat[:spt] # if valid heating setpoints
978
- surface[:cooling] = cool[:spt] if cool[:spt] # if valid cooling setpoints
978
+ # Recover if valid setpoints.
979
+ surface[:heating] = heat[:spt] if heat && heat[:spt]
980
+ surface[:cooling] = cool[:spt] if cool && cool[:spt]
979
981
 
980
982
  tbd[:surfaces][s.nameString] = surface
981
983
  end # (opaque) surfaces populated
@@ -1174,9 +1176,6 @@ module TBD
1174
1176
  farthest_V = origin_point_V if farther
1175
1177
  end
1176
1178
 
1177
- puts "ADDITION!!" if id == "ADDITION"
1178
- puts "#{reference_V} vs #{farthest_V}" if id == "ADDITION"
1179
-
1180
1179
  angle = reference_V.angle(farthest_V)
1181
1180
  invalid("#{id} polar angle", mth, 0, ERROR, 0) if angle.nil?
1182
1181
  angle = 0 if angle.nil?
@@ -1488,7 +1487,7 @@ module TBD
1488
1487
 
1489
1488
  edge[:surfaces].keys.each do |i|
1490
1489
  break if is[:rimjoist] || is[:balcony]
1491
- break unless deratables.size == 2
1490
+ break unless deratables.size > 0
1492
1491
  break if floors.key?(id)
1493
1492
  next if i == id
1494
1493
  next unless floors.key?(i)
@@ -1771,13 +1770,120 @@ module TBD
1771
1770
  end
1772
1771
  end
1773
1772
 
1773
+ # Fetch edge multipliers for subsurfaces, if applicable.
1774
+ edges.values.each do |edge|
1775
+ next if edge.key?(:mult) # skip if already assigned
1776
+ next unless edge.key?(:surfaces)
1777
+ next unless edge.key?(:psi)
1778
+ ok = false
1779
+
1780
+ edge[:psi].keys.each do |k|
1781
+ break if ok
1782
+
1783
+ jamb = k.to_s.include?("jamb")
1784
+ sill = k.to_s.include?("sill")
1785
+ head = k.to_s.include?("head")
1786
+ ok = jamb || sill || head
1787
+ end
1788
+
1789
+ next unless ok # if OK, edge links subsurface(s) ... yet which one(s)?
1790
+
1791
+ edge[:surfaces].each do |id, surface|
1792
+ next unless tbd[:surfaces].key?(id) # look up parent (opaque) surface
1793
+
1794
+ [:windows, :doors, :skylights].each do |subtypes|
1795
+ next unless tbd[:surfaces][id].key?(subtypes)
1796
+
1797
+ tbd[:surfaces][id][subtypes].each do |nom, sub|
1798
+ next unless edge[:surfaces].key?(nom)
1799
+ next unless sub[:mult] > 1
1800
+
1801
+ # An edge may be tagged with (potentially conflicting) multipliers.
1802
+ # This is only possible if the edge links 2 subsurfaces, e.g. a
1803
+ # shared jamb between window & door. By default, TBD tags common
1804
+ # subsurface edges as (mild) "transitions" (i.e. PSI 0 W/K.m), so
1805
+ # there would be no point in assigning an edge multiplier. Users
1806
+ # can however reset an edge type via a TBD JSON input file (e.g.
1807
+ # "joint" instead of "transition"). It would be a very odd choice,
1808
+ # but TBD doesn't prohibit it. If linked subsurfaces have different
1809
+ # multipliers (e.g. 2 vs 3), TBD tracks the highest value.
1810
+ edge[:mult] = sub[:mult] unless edge.key?(:mult)
1811
+ edge[:mult] = sub[:mult] if sub[:mult] > edge[:mult]
1812
+ end
1813
+ end
1814
+ end
1815
+ end
1816
+
1817
+ # Unless a user has set the thermal bridge type of an individual edge via
1818
+ # JSON input, reset any subsurface's head, sill or jamb edges as (mild)
1819
+ # transitions when in close proximity to another subsurface edge. Both
1820
+ # edges' origin and terminal vertices must be in close proximity. Edges
1821
+ # of unhinged subsurfaces are ignored.
1822
+ edges.each do |id, edge|
1823
+ nb = 0 # linked subsurfaces (i.e. "holes")
1824
+ match = false
1825
+ next if edge.key?(:io_type) # skip if set in JSON
1826
+ next unless edge.key?(:v0)
1827
+ next unless edge.key?(:v1)
1828
+ next unless edge.key?(:psi)
1829
+ next unless edge.key?(:surfaces)
1830
+
1831
+ edge[:surfaces].keys.each do |identifier|
1832
+ break if match
1833
+ next unless holes.key?(identifier)
1834
+
1835
+ if holes[identifier].attributes.key?(:unhinged)
1836
+ nb = 0 if holes[identifier].attributes[:unhinged]
1837
+ break if holes[identifier].attributes[:unhinged]
1838
+ end
1839
+
1840
+ nb += 1
1841
+ match = true if nb > 1
1842
+ end
1843
+
1844
+ if nb == 1 # linking 1x subsurface, search for 1x other.
1845
+ e1 = { v0: edge[:v0].point, v1: edge[:v1].point }
1846
+
1847
+ edges.each do |nom, e|
1848
+ nb = 0
1849
+ break if match
1850
+ next if nom == id
1851
+ next if e.key?(:io_type)
1852
+ next unless e.key?(:psi)
1853
+ next unless e.key?(:surfaces)
1854
+
1855
+ e[:surfaces].keys.each do |identifier|
1856
+ next unless holes.key?(identifier)
1857
+
1858
+ if holes[identifier].attributes.key?(:unhinged)
1859
+ nb = 0 if holes[identifier].attributes[:unhinged]
1860
+ break if holes[identifier].attributes[:unhinged]
1861
+ end
1862
+
1863
+ nb += 1
1864
+ end
1865
+
1866
+ next unless nb == 1 # only process edge if linking 1x subsurface
1867
+
1868
+ e2 = { v0: e[:v0].point, v1: e[:v1].point }
1869
+ match = matches?(e1, e2, argh[:sub_tol])
1870
+ end
1871
+ end
1872
+
1873
+ next unless match
1874
+
1875
+ edge[:psi] = { transition: 0.000 }
1876
+ edge[:set] = json[:io][:building][:psi]
1877
+ end
1878
+
1774
1879
  # Loop through each edge and assign heat loss to linked surfaces.
1775
1880
  edges.each do |identifier, edge|
1776
1881
  next unless edge.key?(:psi)
1777
1882
  rsi = 0
1778
- max = edge[:psi].values.max
1779
- type = edge[:psi].key(max)
1883
+ max = edge[:psi ].values.max
1884
+ type = edge[:psi ].key(max)
1780
1885
  length = edge[:length]
1886
+ length *= edge[:mult ] if edge.key?(:mult)
1781
1887
  bridge = { psi: max, type: type, length: length }
1782
1888
  deratables = {}
1783
1889
  apertures = {}
@@ -1869,7 +1975,7 @@ module TBD
1869
1975
  # ... first 'uprate' targeted insulation layers (see ua.rb) before derating.
1870
1976
  # Check for new argh keys [:wall_uo], [:roof_uo] and/or [:floor_uo].
1871
1977
  up = argh[:uprate_walls] || argh[:uprate_roofs] || argh[:uprate_floors]
1872
- uprate(model, tbd[:surfaces], argh) if up
1978
+ uprate(model, tbd[:surfaces], argh) if up
1873
1979
 
1874
1980
  # Derated (cloned) constructions are unique to each deratable surface.
1875
1981
  # Unique construction names are prefixed with the surface name,
@@ -1975,6 +2081,7 @@ module TBD
1975
2081
  set = e[:set]
1976
2082
  t = e[:psi].key(v)
1977
2083
  l = e[:length]
2084
+ l *= e[:mult] if e.key?(:mult)
1978
2085
  edge = { psi: set, type: t, length: l, surfaces: e[:surfaces].keys }
1979
2086
  edge[:v0x] = e[:v0].point.x
1980
2087
  edge[:v0y] = e[:v0].point.y