tbd 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 )
@@ -1174,9 +1175,6 @@ module TBD
1174
1175
  farthest_V = origin_point_V if farther
1175
1176
  end
1176
1177
 
1177
- puts "ADDITION!!" if id == "ADDITION"
1178
- puts "#{reference_V} vs #{farthest_V}" if id == "ADDITION"
1179
-
1180
1178
  angle = reference_V.angle(farthest_V)
1181
1179
  invalid("#{id} polar angle", mth, 0, ERROR, 0) if angle.nil?
1182
1180
  angle = 0 if angle.nil?
@@ -1771,13 +1769,120 @@ module TBD
1771
1769
  end
1772
1770
  end
1773
1771
 
1772
+ # Fetch edge multipliers for subsurfaces, if applicable.
1773
+ edges.values.each do |edge|
1774
+ next if edge.key?(:mult) # skip if already assigned
1775
+ next unless edge.key?(:surfaces)
1776
+ next unless edge.key?(:psi)
1777
+ ok = false
1778
+
1779
+ edge[:psi].keys.each do |k|
1780
+ break if ok
1781
+
1782
+ jamb = k.to_s.include?("jamb")
1783
+ sill = k.to_s.include?("sill")
1784
+ head = k.to_s.include?("head")
1785
+ ok = jamb || sill || head
1786
+ end
1787
+
1788
+ next unless ok # if OK, edge links subsurface(s) ... yet which one(s)?
1789
+
1790
+ edge[:surfaces].each do |id, surface|
1791
+ next unless tbd[:surfaces].key?(id) # look up parent (opaque) surface
1792
+
1793
+ [:windows, :doors, :skylights].each do |subtypes|
1794
+ next unless tbd[:surfaces][id].key?(subtypes)
1795
+
1796
+ tbd[:surfaces][id][subtypes].each do |nom, sub|
1797
+ next unless edge[:surfaces].key?(nom)
1798
+ next unless sub[:mult] > 1
1799
+
1800
+ # An edge may be tagged with (potentially conflicting) multipliers.
1801
+ # This is only possible if the edge links 2 subsurfaces, e.g. a
1802
+ # shared jamb between window & door. By default, TBD tags common
1803
+ # subsurface edges as (mild) "transitions" (i.e. PSI 0 W/K.m), so
1804
+ # there would be no point in assigning an edge multiplier. Users
1805
+ # can however reset an edge type via a TBD JSON input file (e.g.
1806
+ # "joint" instead of "transition"). It would be a very odd choice,
1807
+ # but TBD doesn't prohibit it. If linked subsurfaces have different
1808
+ # multipliers (e.g. 2 vs 3), TBD tracks the highest value.
1809
+ edge[:mult] = sub[:mult] unless edge.key?(:mult)
1810
+ edge[:mult] = sub[:mult] if sub[:mult] > edge[:mult]
1811
+ end
1812
+ end
1813
+ end
1814
+ end
1815
+
1816
+ # Unless a user has set the thermal bridge type of an individual edge via
1817
+ # JSON input, reset any subsurface's head, sill or jamb edges as (mild)
1818
+ # transitions when in close proximity to another subsurface edge. Both
1819
+ # edges' origin and terminal vertices must be in close proximity. Edges
1820
+ # of unhinged subsurfaces are ignored.
1821
+ edges.each do |id, edge|
1822
+ nb = 0 # linked subsurfaces (i.e. "holes")
1823
+ match = false
1824
+ next if edge.key?(:io_type) # skip if set in JSON
1825
+ next unless edge.key?(:v0)
1826
+ next unless edge.key?(:v1)
1827
+ next unless edge.key?(:psi)
1828
+ next unless edge.key?(:surfaces)
1829
+
1830
+ edge[:surfaces].keys.each do |identifier|
1831
+ break if match
1832
+ next unless holes.key?(identifier)
1833
+
1834
+ if holes[identifier].attributes.key?(:unhinged)
1835
+ nb = 0 if holes[identifier].attributes[:unhinged]
1836
+ break if holes[identifier].attributes[:unhinged]
1837
+ end
1838
+
1839
+ nb += 1
1840
+ match = true if nb > 1
1841
+ end
1842
+
1843
+ if nb == 1 # linking 1x subsurface, search for 1x other.
1844
+ e1 = { v0: edge[:v0].point, v1: edge[:v1].point }
1845
+
1846
+ edges.each do |nom, e|
1847
+ nb = 0
1848
+ break if match
1849
+ next if nom == id
1850
+ next if e.key?(:io_type)
1851
+ next unless e.key?(:psi)
1852
+ next unless e.key?(:surfaces)
1853
+
1854
+ e[:surfaces].keys.each do |identifier|
1855
+ next unless holes.key?(identifier)
1856
+
1857
+ if holes[identifier].attributes.key?(:unhinged)
1858
+ nb = 0 if holes[identifier].attributes[:unhinged]
1859
+ break if holes[identifier].attributes[:unhinged]
1860
+ end
1861
+
1862
+ nb += 1
1863
+ end
1864
+
1865
+ next unless nb == 1 # only process edge if linking 1x subsurface
1866
+
1867
+ e2 = { v0: e[:v0].point, v1: e[:v1].point }
1868
+ match = matches?(e1, e2, argh[:sub_tol])
1869
+ end
1870
+ end
1871
+
1872
+ next unless match
1873
+
1874
+ edge[:psi] = { transition: 0.000 }
1875
+ edge[:set] = json[:io][:building][:psi]
1876
+ end
1877
+
1774
1878
  # Loop through each edge and assign heat loss to linked surfaces.
1775
1879
  edges.each do |identifier, edge|
1776
1880
  next unless edge.key?(:psi)
1777
1881
  rsi = 0
1778
- max = edge[:psi].values.max
1779
- type = edge[:psi].key(max)
1882
+ max = edge[:psi ].values.max
1883
+ type = edge[:psi ].key(max)
1780
1884
  length = edge[:length]
1885
+ length *= edge[:mult ] if edge.key?(:mult)
1781
1886
  bridge = { psi: max, type: type, length: length }
1782
1887
  deratables = {}
1783
1888
  apertures = {}
@@ -1869,7 +1974,7 @@ module TBD
1869
1974
  # ... first 'uprate' targeted insulation layers (see ua.rb) before derating.
1870
1975
  # Check for new argh keys [:wall_uo], [:roof_uo] and/or [:floor_uo].
1871
1976
  up = argh[:uprate_walls] || argh[:uprate_roofs] || argh[:uprate_floors]
1872
- uprate(model, tbd[:surfaces], argh) if up
1977
+ uprate(model, tbd[:surfaces], argh) if up
1873
1978
 
1874
1979
  # Derated (cloned) constructions are unique to each deratable surface.
1875
1980
  # Unique construction names are prefixed with the surface name,
@@ -1975,6 +2080,7 @@ module TBD
1975
2080
  set = e[:set]
1976
2081
  t = e[:psi].key(v)
1977
2082
  l = e[:length]
2083
+ l *= e[:mult] if e.key?(:mult)
1978
2084
  edge = { psi: set, type: t, length: l, surfaces: e[:surfaces].keys }
1979
2085
  edge[:v0x] = e[:v0].point.x
1980
2086
  edge[:v0y] = e[:v0].point.y