tbd 3.4.4 → 3.5.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.
@@ -1,121 +1,121 @@
1
- require 'matrix'
2
-
3
- class Matrix
4
- def []=(i, j, x)
5
- @rows[i][j] = x
6
- end
7
- end
8
-
9
- module Topolys
10
-
11
- # Transformations can be applied to Geometry. Ported from OpenStudio Transformation library.
12
-
13
- class Transformation
14
-
15
- # @return [Matrix] internal 4x4 matrix
16
- attr_reader :matrix
17
-
18
- ##
19
- # Initializes an Transformation object
20
- #
21
- # @param [Matrix] matrix A 4x4 matrix, defaults to identity
22
- def initialize(matrix=Matrix.identity(4))
23
- raise "Incorrect argument for Transformation, expected Matrix but got #{matrix.class}" unless matrix.is_a?(Matrix)
24
- @matrix = matrix
25
- end
26
-
27
- # translation along vector
28
- def Transformation.translation(translation)
29
- return nil if !translation.is_a?(Vector3D)
30
-
31
- matrix = Matrix.identity(4)
32
- matrix[0,3] = translation.x
33
- matrix[1,3] = translation.y
34
- matrix[2,3] = translation.z
35
-
36
- return Transformation.new(matrix)
37
- end
38
-
39
- ##
40
- # Initializes a rotation about origin defined by axis and angle (radians)
41
- #
42
- def Transformation.rotation(axis, radians)
43
- return nil if !axis.is_a?(Vector3D)
44
- return nil if !radians.is_a?(Numeric)
45
- return nil if (axis.magnitude < Float::EPSILON)
46
- normal = axis
47
- normal.normalize!
48
-
49
- # Rodrigues' rotation formula / Rotation matrix from Euler axis/angle
50
- # I*cos(radians) + I*(1-cos(radians))*axis*axis^T + Q*sin(radians)
51
- # Q = [0, -axis[2], axis[1]; axis[2], 0, -axis[0]; -axis[1], axis[0], 0]
52
- p = normal.outer_product(normal)
53
- i = Matrix.identity(3)
54
- q = Matrix.zero(3)
55
- q[0,1] = -normal.z
56
- q[0,2] = normal.y
57
- q[1,0] = normal.z
58
- q[1,2] = -normal.x
59
- q[2,0] = -normal.y
60
- q[2,1] = normal.x
61
-
62
- # rotation matrix
63
- r = i*Math.cos(radians) + (1-Math.cos(radians))*p + q*Math.sin(radians)
64
-
65
- matrix = Matrix.identity(4)
66
- matrix[0,0] = r[0,0]
67
- matrix[0,1] = r[0,1]
68
- matrix[0,2] = r[0,2]
69
- matrix[1,0] = r[1,0]
70
- matrix[1,1] = r[1,1]
71
- matrix[1,2] = r[1,2]
72
- matrix[2,0] = r[2,0]
73
- matrix[2,1] = r[2,1]
74
- matrix[2,2] = r[2,2]
75
-
76
- return Transformation.new(matrix)
77
- end
78
-
79
- ##
80
- # Multiplies a Transformation by geometry class
81
- #
82
- # @param [Obj] obj A geometry object
83
- #
84
- # @return [Obj] Returns a new, transformed object - nil if not a geometry object
85
- def *(obj)
86
- if obj.is_a?(Point3D)
87
- return mult_point(obj)
88
- elsif obj.is_a?(Vector3D)
89
- return mult_vector(obj)
90
- elsif obj.is_a?(Array)
91
- return mult_array(obj)
92
- elsif obj.is_a?(Transformation)
93
- return mult_transformation(obj)
94
- end
95
- return nil
96
- end
97
-
98
- private
99
-
100
- def mult_point(point)
101
- temp = Matrix.column_vector([point.x, point.y, point.z, 1])
102
- temp = @matrix*temp
103
- return Point3D.new(temp[0,0],temp[1,0],temp[2,0])
104
- end
105
-
106
- def mult_vector(vector)
107
- temp = Matrix.column_vector([vector.x, vector.y, vector.z, 1])
108
- temp = @matrix*temp
109
- return Vector3D.new(temp[0,0],temp[1,0],temp[2,0])
110
- end
111
-
112
- def mult_array(array)
113
- array.map {|obj| self*obj}
114
- end
115
-
116
- def mult_transformation(obj)
117
- Transformation.new(@matrix * obj.matrix)
118
- end
119
-
120
- end
1
+ require 'matrix'
2
+
3
+ class Matrix
4
+ def []=(i, j, x)
5
+ @rows[i][j] = x
6
+ end
7
+ end
8
+
9
+ module Topolys
10
+
11
+ # Transformations can be applied to Geometry. Ported from OpenStudio Transformation library.
12
+
13
+ class Transformation
14
+
15
+ # @return [Matrix] internal 4x4 matrix
16
+ attr_reader :matrix
17
+
18
+ ##
19
+ # Initializes an Transformation object
20
+ #
21
+ # @param [Matrix] matrix A 4x4 matrix, defaults to identity
22
+ def initialize(matrix=Matrix.identity(4))
23
+ raise "Incorrect argument for Transformation, expected Matrix but got #{matrix.class}" unless matrix.is_a?(Matrix)
24
+ @matrix = matrix
25
+ end
26
+
27
+ # translation along vector
28
+ def Transformation.translation(translation)
29
+ return nil if !translation.is_a?(Vector3D)
30
+
31
+ matrix = Matrix.identity(4)
32
+ matrix[0,3] = translation.x
33
+ matrix[1,3] = translation.y
34
+ matrix[2,3] = translation.z
35
+
36
+ return Transformation.new(matrix)
37
+ end
38
+
39
+ ##
40
+ # Initializes a rotation about origin defined by axis and angle (radians)
41
+ #
42
+ def Transformation.rotation(axis, radians)
43
+ return nil if !axis.is_a?(Vector3D)
44
+ return nil if !radians.is_a?(Numeric)
45
+ return nil if (axis.magnitude < Float::EPSILON)
46
+ normal = axis
47
+ normal.normalize!
48
+
49
+ # Rodrigues' rotation formula / Rotation matrix from Euler axis/angle
50
+ # I*cos(radians) + I*(1-cos(radians))*axis*axis^T + Q*sin(radians)
51
+ # Q = [0, -axis[2], axis[1]; axis[2], 0, -axis[0]; -axis[1], axis[0], 0]
52
+ p = normal.outer_product(normal)
53
+ i = Matrix.identity(3)
54
+ q = Matrix.zero(3)
55
+ q[0,1] = -normal.z
56
+ q[0,2] = normal.y
57
+ q[1,0] = normal.z
58
+ q[1,2] = -normal.x
59
+ q[2,0] = -normal.y
60
+ q[2,1] = normal.x
61
+
62
+ # rotation matrix
63
+ r = i*Math.cos(radians) + (1-Math.cos(radians))*p + q*Math.sin(radians)
64
+
65
+ matrix = Matrix.identity(4)
66
+ matrix[0,0] = r[0,0]
67
+ matrix[0,1] = r[0,1]
68
+ matrix[0,2] = r[0,2]
69
+ matrix[1,0] = r[1,0]
70
+ matrix[1,1] = r[1,1]
71
+ matrix[1,2] = r[1,2]
72
+ matrix[2,0] = r[2,0]
73
+ matrix[2,1] = r[2,1]
74
+ matrix[2,2] = r[2,2]
75
+
76
+ return Transformation.new(matrix)
77
+ end
78
+
79
+ ##
80
+ # Multiplies a Transformation by geometry class
81
+ #
82
+ # @param [Obj] obj A geometry object
83
+ #
84
+ # @return [Obj] Returns a new, transformed object - nil if not a geometry object
85
+ def *(obj)
86
+ if obj.is_a?(Point3D)
87
+ return mult_point(obj)
88
+ elsif obj.is_a?(Vector3D)
89
+ return mult_vector(obj)
90
+ elsif obj.is_a?(Array)
91
+ return mult_array(obj)
92
+ elsif obj.is_a?(Transformation)
93
+ return mult_transformation(obj)
94
+ end
95
+ return nil
96
+ end
97
+
98
+ private
99
+
100
+ def mult_point(point)
101
+ temp = Matrix.column_vector([point.x, point.y, point.z, 1])
102
+ temp = @matrix*temp
103
+ return Point3D.new(temp[0,0],temp[1,0],temp[2,0])
104
+ end
105
+
106
+ def mult_vector(vector)
107
+ temp = Matrix.column_vector([vector.x, vector.y, vector.z, 1])
108
+ temp = @matrix*temp
109
+ return Vector3D.new(temp[0,0],temp[1,0],temp[2,0])
110
+ end
111
+
112
+ def mult_array(array)
113
+ array.map {|obj| self*obj}
114
+ end
115
+
116
+ def mult_transformation(obj)
117
+ Transformation.new(@matrix * obj.matrix)
118
+ end
119
+
120
+ end
121
121
  end
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
  #
3
- # Copyright (c) 2020-2024 Denis Bourgeois & Dan Macumber
3
+ # Copyright (c) 2020-2025 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
@@ -54,12 +54,12 @@ module TBD
54
54
  lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
55
55
  lyr[:index] = nil unless lyr[:index] >= 0
56
56
  lyr[:index] = nil unless lyr[:index] < lc.layers.size
57
- return invalid("#{id} layer index", mth, 3, ERR, res) unless lyr[:index]
57
+ return invalid("#{id} layer index", mth, 3, WRN, res) unless lyr[:index]
58
58
  return zero("#{id}: heatloss" , mth, WRN, res) unless hloss > TOL
59
59
  return zero("#{id}: films" , mth, WRN, res) unless film > TOL
60
- return zero("#{id}: Ut" , mth, WRN, res) unless ut > TOL
61
- return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < 5.678
62
- return zero("#{id}: net area (m2)", mth, ERR, res) unless area > TOL
60
+ return zero("#{id}: Ut" , mth, WRN, res) unless ut > UMIN
61
+ return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < UMAX
62
+ return zero("#{id}: net area (m2)", mth, WRN, res) unless area > TOL
63
63
 
64
64
  # First, calculate initial layer RSi to initially meet Ut target.
65
65
  rt = 1 / ut # target construction Rt
@@ -71,56 +71,51 @@ module TBD
71
71
  u_psi = hloss / area # from psi+khi
72
72
  new_u -= u_psi # uprated layer USi to counter psi+khi
73
73
  new_r = 1 / new_u # uprated layer RSi to counter psi+khi
74
- return zero("#{id}: new Rsi", mth, ERR, res) unless new_r > 0.001
74
+ return zero("#{id}: new Rsi", mth, WRN, res) unless new_r > RMIN
75
75
 
76
76
  if lyr[:type] == :massless
77
- m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
78
- return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
79
-
80
- m = m.get.clone(model).to_MasslessOpaqueMaterial.get
81
- m.setName("#{id} uprated")
82
- new_r = 0.001 unless new_r > 0.001
83
- loss = (new_u - 1 / new_r) * area unless new_r > 0.001
84
- m.setThermalResistance(new_r)
85
- else # type == :standard
86
- m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
87
- return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
88
-
89
- m = m.get.clone(model).to_StandardOpaqueMaterial.get
90
- m.setName("#{id} uprated")
91
- k = m.thermalConductivity
92
-
93
- if new_r > 0.001
94
- d = new_r * k
95
-
96
- unless d > 0.003
97
- d = 0.003
98
- k = d / new_r
99
- k = 3.0 unless k < 3.0
100
- loss = (new_u - k / d) * area unless k < 3.0
101
- end
102
- else # new_r < 0.001 m2•K/W
103
- d = 0.001 * k
104
- d = 0.003 unless d > 0.003
105
- k = d / 0.001 unless d > 0.003
106
- loss = (new_u - k / d) * area
77
+ m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
78
+ return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
79
+
80
+ m = m.get.clone(model).to_MasslessOpaqueMaterial.get
81
+ m.setName("#{id} uprated")
82
+
83
+ new_r = RMIN unless new_r > RMIN
84
+ loss = (new_u - 1 / new_r) * area unless new_r > RMIN
85
+
86
+ unless m.setThermalResistance(new_r)
87
+ return invalid("Can't uprate #{id}: RSi#{new_r.round(2)}", mth, 0, DBG, res)
107
88
  end
89
+ else
90
+ m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
91
+ return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
108
92
 
109
- if m.setThickness(d)
110
- m.setThermalConductivity(k)
111
- else
112
- return invalid("Can't uprate #{id}: #{d} > 3m", mth, 0, ERR, res)
93
+ m = m.get.clone(model).to_StandardOpaqueMaterial.get
94
+ m.setName("#{id} uprated")
95
+
96
+ d = m.thickness
97
+ k = (d / new_r).clamp(KMIN, KMAX)
98
+ d = (k * new_r).clamp(DMIN, DMAX)
99
+
100
+ loss = (new_u - k / d) * area unless d / k > RMIN
101
+
102
+ unless m.setThermalConductivity(k)
103
+ return invalid("Can't uprate #{id}: K#{k.round(3)}", mth, 0, DBG, res)
104
+ end
105
+
106
+ unless m.setThickness(d)
107
+ return invalid("Can't uprate #{id}: #{(d*1000).to_i}mm", mth, 0, DBG, res)
113
108
  end
114
109
  end
115
110
 
116
- return invalid("Can't ID insulating layer", mth, 0, ERR, res) unless m
111
+ return invalid("Can't ID insulating layer", mth, 0, DBG, res) unless m
117
112
 
118
113
  lc.setLayer(lyr[:index], m)
119
114
  uo = 1 / rsi(lc, film)
120
115
 
121
116
  if loss > TOL
122
117
  h_loss = format "%.3f", loss
123
- return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, ERR, res)
118
+ return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, DBG, res)
124
119
  end
125
120
 
126
121
  res[:uo] = uo
@@ -145,9 +140,9 @@ module TBD
145
140
  # @option argh [Bool] :uprate_walls (false) whether to uprate walls
146
141
  # @option argh [Bool] :uprate_roofs (false) whether to uprate roofs
147
142
  # @option argh [Bool] :uprate_floors (false) whether to uprate floors
148
- # @option argh [#to_f] :wall_ut (5.678) uprated wall Usi-factor target
149
- # @option argh [#to_f] :roof_ut (5.678) uprated roof Usi-factor target
150
- # @option argh [#to_f] :floor_ut (5.678) uprated floor Usi-factor target
143
+ # @option argh [#to_f] :wall_ut (UMAX) uprated wall Usi-factor target
144
+ # @option argh [#to_f] :roof_ut (UMAX) uprated roof Usi-factor target
145
+ # @option argh [#to_f] :floor_ut (UMAX) uprated floor Usi-factor target
151
146
  # @option argh [#to_s] :wall_option ("") construction to uprate (or "all")
152
147
  # @option argh [#to_s] :roof_option ("") construction to uprate (or "all")
153
148
  # @option argh [#to_s] :floor_option ("") construction to uprate (or "all")
@@ -172,9 +167,9 @@ module TBD
172
167
  argh[:uprate_walls ] = false unless argh.key?(:uprate_walls)
173
168
  argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs)
174
169
  argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
175
- argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut)
176
- argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut)
177
- argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut)
170
+ argh[:wall_ut ] = UMAX unless argh.key?(:wall_ut)
171
+ argh[:roof_ut ] = UMAX unless argh.key?(:roof_ut)
172
+ argh[:floor_ut ] = UMAX unless argh.key?(:floor_ut)
178
173
  argh[:wall_option ] = "" unless argh.key?(:wall_option)
179
174
  argh[:roof_option ] = "" unless argh.key?(:roof_option)
180
175
  argh[:floor_option ] = "" unless argh.key?(:floor_option)
@@ -197,7 +192,7 @@ module TBD
197
192
  groups.each do |type, g|
198
193
  next unless g[:up]
199
194
  next unless g[:ut].is_a?(Numeric)
200
- next unless g[:ut] < 5.678
195
+ next unless g[:ut] < UMAX
201
196
  next if g[:ut] < 0
202
197
 
203
198
  typ = type
@@ -211,7 +206,7 @@ module TBD
211
206
  all = tout.include?(op)
212
207
 
213
208
  if g[:op].empty?
214
- log(ERR, "Construction (#{type}) to uprate? (#{mth})")
209
+ log(WRN, "Construction (#{type}) to uprate? (#{mth})")
215
210
  elsif all
216
211
  s.each do |nom, surface|
217
212
  next unless surface.key?(:deratable )
@@ -247,11 +242,11 @@ module TBD
247
242
  else
248
243
  id = g[:op]
249
244
  lc = model.getConstructionByName(id)
250
- log(ERR, "Construction '#{id}'? (#{mth})") if lc.empty?
245
+ log(WRN, "Construction '#{id}'? (#{mth})") if lc.empty?
251
246
  next if lc.empty?
252
247
 
253
248
  lc = lc.get.to_LayeredConstruction
254
- log(ERR, "'#{id}' layered construction? (#{mth})") if lc.empty?
249
+ log(WRN, "'#{id}' layered construction? (#{mth})") if lc.empty?
255
250
  next if lc.empty?
256
251
 
257
252
  lc = lc.get
@@ -282,7 +277,7 @@ module TBD
282
277
  end
283
278
 
284
279
  if coll.empty?
285
- log(ERR, "No #{type} construction to uprate - skipping (#{mth})")
280
+ log(WRN, "No #{type} construction to uprate - skipping (#{mth})")
286
281
  next
287
282
  elsif lc
288
283
  # Valid layered construction - good to uprate!
@@ -291,7 +286,7 @@ module TBD
291
286
  lyr[:index] = nil unless lyr[:index] >= 0
292
287
  lyr[:index] = nil unless lyr[:index] < lc.layers.size
293
288
 
294
- log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
289
+ log(WRN, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
295
290
  next unless lyr[:index]
296
291
 
297
292
  # Ensure lc is exclusively linked to deratable surfaces of right type.
@@ -302,10 +297,10 @@ module TBD
302
297
  next unless surface.key?(:construction)
303
298
  next unless surface[:construction].is_a?(cl3)
304
299
  next unless surface[:construction] == lc
300
+ next unless surface[:deratable]
305
301
 
306
302
  ok = true
307
- ok = false unless surface[:type ] == typ
308
- ok = false unless surface[:deratable]
303
+ ok = false unless surface[:type] == typ
309
304
  ok = false unless coll.key?(id)
310
305
  ok = false unless coll[id][:s].key?(nom)
311
306
 
@@ -346,13 +341,18 @@ module TBD
346
341
 
347
342
  if sss.isConstructionDefaulted
348
343
  set = defaultConstructionSet(sss) # building? story?
349
- constructions = set.defaultExteriorSurfaceConstructions
350
344
 
351
- unless constructions.empty?
352
- constructions = constructions.get
353
- constructions.setWallConstruction(lc) if typ == :wall
354
- constructions.setFloorConstruction(lc) if typ == :floor
355
- constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
345
+ if set.nil?
346
+ sss.setConstruction(lc)
347
+ else
348
+ constructions = set.defaultExteriorSurfaceConstructions
349
+
350
+ unless constructions.empty?
351
+ constructions = constructions.get
352
+ constructions.setWallConstruction(lc) if typ == :wall
353
+ constructions.setFloorConstruction(lc) if typ == :floor
354
+ constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
355
+ end
356
356
  end
357
357
  else
358
358
  sss.setConstruction(lc)
@@ -385,7 +385,7 @@ module TBD
385
385
  res = uo(model, lc, id, hloss, film, g[:ut])
386
386
 
387
387
  unless res[:uo] && res[:m]
388
- log(ERR, "Unable to uprate '#{id}' (#{mth})")
388
+ log(WRN, "Unable to uprate '#{id}' (#{mth})")
389
389
  next
390
390
  end
391
391
 
@@ -407,7 +407,7 @@ module TBD
407
407
  argh[:roof_uo ] = res[:uo] if typ == :ceiling
408
408
  argh[:floor_uo] = res[:uo] if typ == :floor
409
409
  else
410
- log(ERR, "Nilled construction to uprate - (#{mth})")
410
+ log(WRN, "Nilled construction to uprate - (#{mth})")
411
411
  return false
412
412
  end
413
413
  end
@@ -589,7 +589,7 @@ module TBD
589
589
  empty = shorts[:has].empty? && shorts[:val].empty?
590
590
  has = shorts[:has] unless empty
591
591
  val = shorts[:val] unless empty
592
- log(ERR, "Invalid UA' reference set (#{mth})") if empty
592
+ log(WRN, "Invalid UA' reference set (#{mth})") if empty
593
593
 
594
594
  unless empty
595
595
  ua[:model] += " : Design vs '#{ref}'"
@@ -1000,7 +1000,7 @@ module TBD
1000
1000
  model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
1001
1001
  model += " (v#{ua[:version]})" if ua.key?(:version)
1002
1002
  report << model unless model.empty?
1003
- report << "* TBD : v3.4.4"
1003
+ report << "* TBD : v3.5.0"
1004
1004
  report << "* date : #{ua[:date]}"
1005
1005
 
1006
1006
  if lang == :en