diff --git a/lib/measures/tbd/measure.xml b/lib/measures/tbd/measure.xml
index 5d17eae..f9048ef 100644
--- a/lib/measures/tbd/measure.xml
+++ b/lib/measures/tbd/measure.xml
@@ -3,8 +3,8 @@
3.1
tbd_measure
8890787b-8c25-4dc8-8641-b6be1b6c2357
- 7a2d773a-7a50-4c69-aa1f-93ed796e9ace
- 2025-08-15T13:39:31Z
+ 0e1528a0-a176-4d46-82d9-d2da40c0abd7
+ 2025-09-11T11:27:14Z
99772807
TBDMeasure
Thermal Bridging and Derating - TBD
@@ -499,7 +499,7 @@
geo.rb
rb
resource
- EF3BA8F7
+ 5AB24CFB
geometry.rb
@@ -523,7 +523,7 @@
psi.rb
rb
resource
- 71AED953
+ 29905280
tbd.rb
@@ -541,13 +541,13 @@
ua.rb
rb
resource
- 022F6D10
+ 0CC24D82
utils.rb
rb
resource
- 3CD8019A
+ 6940D006
version.rb
diff --git a/lib/measures/tbd/resources/geo.rb b/lib/measures/tbd/resources/geo.rb
index 7dd2a6f..34e8f07 100644
--- a/lib/measures/tbd/resources/geo.rb
+++ b/lib/measures/tbd/resources/geo.rb
@@ -310,23 +310,24 @@ def properties(surface = nil, argh = {})
end
unless surface.construction.empty?
- construction = surface.construction.get.to_LayeredConstruction
+ lc = surface.construction.get.to_LayeredConstruction
- unless construction.empty?
- construction = construction.get
- lyr = insulatingLayer(construction)
- lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
- lyr[:index] = nil unless lyr[:index] >= 0
- lyr[:index] = nil unless lyr[:index] < construction.layers.size
+ unless lc.empty?
+ lc = lc.get
+ lyr = insulatingLayer(lc)
- if lyr[:index]
- surf[:construction] = construction
+ if lyr[:index].is_a?(Integer) && lyr[:index].between?(0, lc.numLayers - 1)
+ surf[:construction] = lc
# index: ... of layer/material (to derate) within construction
# ltype: either :massless (RSi) or :standard (k + d)
# r : initial RSi value of the indexed layer to derate
surf[:index] = lyr[:index]
surf[:ltype] = lyr[:type ]
surf[:r ] = lyr[:r ]
+ else
+ surf[:index] = nil
+ surf[:ltype] = nil
+ surf[:r ] = 0.0
end
end
end
diff --git a/lib/measures/tbd/resources/psi.rb b/lib/measures/tbd/resources/psi.rb
index 8075770..d80874c 100644
--- a/lib/measures/tbd/resources/psi.rb
+++ b/lib/measures/tbd/resources/psi.rb
@@ -1355,103 +1355,103 @@ def inputs(s = {}, e = {}, argh = {})
##
# Thermally derates insulating material within construction.
#
- # @param id [#to_s] surface identifier
+ # @param id [#to_sym] surface identifier
# @param [Hash] s TBD surface parameters
- # @option s [#to_f] :heatloss heat loss from major thermal bridging, in W/K
- # @option s [#to_f] :net surface net area, in m2
+ # @option s [Numeric] :heatloss heat loss from major thermal bridging, in W/K
+ # @option s [Numeric] :net surface net area, in m2
# @option s [:massless, :standard] :ltype indexed layer type
- # @option s [#to_i] :index deratable construction layer index
- # @option s [#to_f] :r deratable layer Rsi-factor, in m2•K/W
+ # @option s [Integer] :index deratable construction layer index
+ # @option s [Numeric] :r deratable layer Rsi-factor, in m2•K/W
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
#
- # @return [OpenStudio::Model::Material] derated (cloned) material
+ # @return [OpenStudio::Model::OpaqueMaterial] derated (cloned) material
# @return [nil] if invalid input (see logs)
def derate(id = "", s = {}, lc = nil)
mth = "TBD::#{__callee__}"
m = nil
- id = trim(id)
kys = [:heatloss, :net, :ltype, :index, :r]
- ck1 = s.is_a?(Hash)
- ck2 = lc.is_a?(OpenStudio::Model::LayeredConstruction)
- return mismatch("id" , id, cl6, mth) if id.empty?
- return mismatch("#{id} surface" , s , cl1, mth) unless ck1
- return mismatch("#{id} construction", lc, cl2, mth) unless ck2
+ cl = OpenStudio::Model::LayeredConstruction
+ return mismatch("lc", lc, cl, mth) unless lc.respond_to?(NS)
+ return mismatch("id", id, String, mth) unless id.respond_to?(:to_sym)
+
+ id = trim(id)
+ nom = lc.nameString
+ return invalid("id", mth, 1) if id.empty?
+ return mismatch(nom, lc, cl, mth) unless lc.is_a?(cl)
+ return mismatch("#{nom} surface", s, Hash, mth) unless s.is_a?(Hash)
+
+ if nom.downcase.include?(" tbd")
+ log(WRN, "Won't derate '#{nom}': tagged as derated (#{mth})")
+ return m
+ end
kys.each do |k|
tag = "#{id} #{k}"
- return hashkey(tag, s, k, mth, ERR) unless s.key?(k)
+ return hashkey(tag, s, k, mth) unless s.key?(k)
case k
- when :heatloss
- return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_f)
- return zero(tag, mth, WRN) if s[k].to_f.abs < 0.001
- when :net, :r
- return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_f)
- return negative(tag, mth, 2, ERR) if s[k].to_f < 0
- return zero(tag, mth, WRN) if s[k].to_f.abs < 0.001
- when :index
- return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_i)
- return negative(tag, mth, 2, ERR) if s[k].to_f < 0
- else # :ltype
+ when :ltype
next if [:massless, :standard].include?(s[k])
- return invalid(tag, mth, 2, ERR)
- end
- end
+ return invalid(tag, mth, 2)
+ when :index
+ return mismatch(tag, s[k], Integer, mth) unless s[k].is_a?(Integer)
+ return invalid(tag, mth, 2) unless s[k].between?(0, lc.numLayers - 1)
+ else
+ return mismatch(tag, s[k], Numeric, mth) unless s[k].is_a?(Numeric)
+ next if k == :heatloss
- if lc.nameString.downcase.include?(" tbd")
- log(WRN, "Won't derate '#{id}': tagged as derated (#{mth})")
- return m
+ return negative(tag, mth, 2) if s[k] < 0
+ return zero(tag, mth) if s[k].abs < 0.001
+ end
end
model = lc.model
- ltype = s[:ltype ]
- index = s[:index ].to_i
- net = s[:net ].to_f
- r = s[:r ].to_f
- u = s[:heatloss].to_f / net
+ ltype = s[:ltype]
+ index = s[:index]
+ net = s[:net]
+ r = s[:r]
+ u = s[:heatloss] / net
loss = 0
- de_u = 1 / r + u # derated U
- de_r = 1 / de_u # derated R
+ de_u = 1 / r + u # derated insulating material U
+ de_r = 1 / de_u # derated insulating material R
if ltype == :massless
- m = lc.getLayer(index).to_MasslessOpaqueMaterial
+ m = lc.getLayer(index).to_MasslessOpaqueMaterial
return invalid("#{id} massless layer?", mth, 0) if m.empty?
- m = m.get
- up = ""
- up = "uprated " if m.nameString.downcase.include?(" uprated")
- m = m.clone(model).to_MasslessOpaqueMaterial.get
- m.setName("#{id} #{up}m tbd")
- de_r = 0.001 unless de_r > 0.001
- loss = (de_u - 1 / de_r) * net unless de_r > 0.001
- m.setThermalResistance(de_r)
+
+ m = m.get
+ up = m.nameString.downcase.include?(" uprated") ? "uprated " : ""
+ m = m.clone(model).to_MasslessOpaqueMaterial.get
+ m.setName("#{id} #{up}m tbd")
+
+ de_r = RMIN unless de_r > RMIN
+ loss = (de_u - 1 / de_r) * net unless de_r > RMIN
+
+ unless m.setThermalResistance(de_r)
+ return invalid("Can't derate #{id}: RSi#{de_r.round(2)}", mth)
+ end
else
- m = lc.getLayer(index).to_StandardOpaqueMaterial
+ m = lc.getLayer(index).to_StandardOpaqueMaterial
return invalid("#{id} standard layer?", mth, 0) if m.empty?
- m = m.get
- up = ""
- up = "uprated " if m.nameString.downcase.include?(" uprated")
- m = m.clone(model).to_StandardOpaqueMaterial.get
- m.setName("#{id} #{up}m tbd")
- k = m.thermalConductivity
-
- if de_r > 0.001
- d = de_r * k
-
- unless d > 0.003
- d = 0.003
- k = d / de_r
- k = 3 unless k < 3
- loss = (de_u - k / d) * net unless k < 3
- end
- else # de_r < 0.001 m2•K/W
- d = 0.001 * k
- d = 0.003 unless d > 0.003
- k = d / 0.001 unless d > 0.003
- loss = (de_u - k / d) * net
+
+ m = m.get
+ up = m.nameString.downcase.include?(" uprated") ? "uprated " : ""
+ m = m.clone(model).to_StandardOpaqueMaterial.get
+ m.setName("#{id} #{up}m tbd")
+
+ d = m.thickness
+ k = (d / de_r).clamp(KMIN, KMAX)
+ d = (k * de_r).clamp(DMIN, DMAX)
+
+ loss = (de_u - k / d) * net unless d / k > RMIN
+
+ unless m.setThermalConductivity(k)
+ return invalid("Can't derate #{id}: K#{k.round(3)}", mth)
end
- m.setThickness(d)
- m.setThermalConductivity(k)
+ unless m.setThickness(d)
+ return invalid("Can't derate #{id}: #{(d*1000).to_i}mm", mth)
+ end
end
if m && loss > TOL
@@ -1941,14 +1941,38 @@ def process(model = nil, argh = {})
ids = windows.keys + doors.keys + skylights.keys
end
- unless ids.include?(i)
- log(ERR, "Orphaned subsurface #{i} (mth)")
+ adj = nil
+
+ unless ids.include?(i) # adjacent sub surface?
+ sb = model.getSubSurfaceByName(i)
+
+ if sb.empty?
+ log(DBG, "Orphaned subsurface #{i} (#{mth})?")
+ else
+ sb = sb.get
+ adj = sb.adjacentSubSurface
+
+ if adj.empty?
+ log(DBG, "Orphaned sub #{i} (#{mth})?")
+ end
+ end
+
next
end
- window = windows.key?(i) ? windows[i] : {}
- door = doors.key?(i) ? doors[i] : {}
- skylight = skylights.key?(i) ? skylights[i] : {}
+ if adj
+ window = windows.key?(i) ? windows[i] : {}
+ door = doors.key?(i) ? doors[i] : {}
+ skylight = skylights.key?(i) ? skylights[i] : {}
+ else
+ window = windows.key?(i) ? windows[i] : {}
+ door = doors.key?(i) ? doors[i] : {}
+ skylight = skylights.key?(i) ? skylights[i] : {}
+ end
+
+ # window = windows.key?(i) ? windows[i] : {}
+ # door = doors.key?(i) ? doors[i] : {}
+ # skylight = skylights.key?(i) ? skylights[i] : {}
sub = window unless window.empty?
sub = door unless door.empty?
@@ -2939,12 +2963,12 @@ def process(model = nil, argh = {})
up = argh[:uprate_walls] || argh[:uprate_roofs] || argh[:uprate_floors]
uprate(model, tbd[:surfaces], argh) if up
- # Derated (cloned) constructions are unique to each deratable surface.
- # Unique construction names are prefixed with the surface name,
- # and suffixed with " tbd", indicating that the construction is
- # henceforth thermally derated. The " tbd" expression is also key in
- # avoiding inadvertent derating - TBD will not derate constructions
- # (or rather layered materials) having " tbd" in their OpenStudio name.
+ # A derated (cloned) construction and (cloned) insulating layer are unique
+ # to each deratable surface. Unique construction and material names are
+ # prefixed with the surface name, and suffixed with " tbd", indicating that
+ # the construction is henceforth thermally derated. The " tbd" expression
+ # is also key in avoiding inadvertent sequential derating - TBD will not
+ # derate a construction/material pair having " tbd" in their OpenStudio name.
tbd[:surfaces].each do |id, surface|
next unless surface.key?(:construction)
next unless surface.key?(:index)
@@ -3138,9 +3162,9 @@ def exit(runner = nil, argh = {})
argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
- argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut )
- argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut )
- argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut )
+ argh[:wall_ut ] = UMAX unless argh.key?(:wall_ut )
+ argh[:roof_ut ] = UMAX unless argh.key?(:roof_ut )
+ argh[:floor_ut ] = UMAX unless argh.key?(:floor_ut )
argh[:wall_option ] = "" unless argh.key?(:wall_option )
argh[:roof_option ] = "" unless argh.key?(:roof_option )
argh[:floor_option ] = "" unless argh.key?(:floor_option )
diff --git a/lib/measures/tbd/resources/ua.rb b/lib/measures/tbd/resources/ua.rb
index 41c1d5b..bfbedf9 100644
--- a/lib/measures/tbd/resources/ua.rb
+++ b/lib/measures/tbd/resources/ua.rb
@@ -54,12 +54,12 @@ def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
lyr[:index] = nil unless lyr[:index] >= 0
lyr[:index] = nil unless lyr[:index] < lc.layers.size
- return invalid("#{id} layer index", mth, 3, ERR, res) unless lyr[:index]
+ return invalid("#{id} layer index", mth, 3, WRN, res) unless lyr[:index]
return zero("#{id}: heatloss" , mth, WRN, res) unless hloss > TOL
return zero("#{id}: films" , mth, WRN, res) unless film > TOL
- return zero("#{id}: Ut" , mth, WRN, res) unless ut > TOL
- return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < 5.678
- return zero("#{id}: net area (m2)", mth, ERR, res) unless area > TOL
+ return zero("#{id}: Ut" , mth, WRN, res) unless ut > UMIN
+ return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < UMAX
+ return zero("#{id}: net area (m2)", mth, WRN, res) unless area > TOL
# First, calculate initial layer RSi to initially meet Ut target.
rt = 1 / ut # target construction Rt
@@ -71,56 +71,51 @@ def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
u_psi = hloss / area # from psi+khi
new_u -= u_psi # uprated layer USi to counter psi+khi
new_r = 1 / new_u # uprated layer RSi to counter psi+khi
- return zero("#{id}: new Rsi", mth, ERR, res) unless new_r > 0.001
+ return zero("#{id}: new Rsi", mth, WRN, res) unless new_r > RMIN
if lyr[:type] == :massless
- m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
- return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
-
- m = m.get.clone(model).to_MasslessOpaqueMaterial.get
- m.setName("#{id} uprated")
- new_r = 0.001 unless new_r > 0.001
- loss = (new_u - 1 / new_r) * area unless new_r > 0.001
- m.setThermalResistance(new_r)
- else # type == :standard
- m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
- return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
-
- m = m.get.clone(model).to_StandardOpaqueMaterial.get
- m.setName("#{id} uprated")
- k = m.thermalConductivity
-
- if new_r > 0.001
- d = new_r * k
-
- unless d > 0.003
- d = 0.003
- k = d / new_r
- k = 3.0 unless k < 3.0
- loss = (new_u - k / d) * area unless k < 3.0
- end
- else # new_r < 0.001 m2•K/W
- d = 0.001 * k
- d = 0.003 unless d > 0.003
- k = d / 0.001 unless d > 0.003
- loss = (new_u - k / d) * area
+ m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
+ return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
+
+ m = m.get.clone(model).to_MasslessOpaqueMaterial.get
+ m.setName("#{id} uprated")
+
+ new_r = RMIN unless new_r > RMIN
+ loss = (new_u - 1 / new_r) * area unless new_r > RMIN
+
+ unless m.setThermalResistance(new_r)
+ return invalid("Can't uprate #{id}: RSi#{new_r.round(2)}", mth, 0, DBG, res)
end
+ else
+ m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
+ return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
- if m.setThickness(d)
- m.setThermalConductivity(k)
- else
- return invalid("Can't uprate #{id}: #{d} > 3m", mth, 0, ERR, res)
+ m = m.get.clone(model).to_StandardOpaqueMaterial.get
+ m.setName("#{id} uprated")
+
+ d = m.thickness
+ k = (d / new_r).clamp(KMIN, KMAX)
+ d = (k * new_r).clamp(DMIN, DMAX)
+
+ loss = (new_u - k / d) * area unless d / k > RMIN
+
+ unless m.setThermalConductivity(k)
+ return invalid("Can't uprate #{id}: K#{k.round(3)}", mth, 0, DBG, res)
+ end
+
+ unless m.setThickness(d)
+ return invalid("Can't uprate #{id}: #{(d*1000).to_i}mm", mth, 0, DBG, res)
end
end
- return invalid("Can't ID insulating layer", mth, 0, ERR, res) unless m
+ return invalid("Can't ID insulating layer", mth, 0, DBG, res) unless m
lc.setLayer(lyr[:index], m)
uo = 1 / rsi(lc, film)
if loss > TOL
h_loss = format "%.3f", loss
- return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, ERR, res)
+ return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, DBG, res)
end
res[:uo] = uo
@@ -145,9 +140,9 @@ def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
# @option argh [Bool] :uprate_walls (false) whether to uprate walls
# @option argh [Bool] :uprate_roofs (false) whether to uprate roofs
# @option argh [Bool] :uprate_floors (false) whether to uprate floors
- # @option argh [#to_f] :wall_ut (5.678) uprated wall Usi-factor target
- # @option argh [#to_f] :roof_ut (5.678) uprated roof Usi-factor target
- # @option argh [#to_f] :floor_ut (5.678) uprated floor Usi-factor target
+ # @option argh [#to_f] :wall_ut (UMAX) uprated wall Usi-factor target
+ # @option argh [#to_f] :roof_ut (UMAX) uprated roof Usi-factor target
+ # @option argh [#to_f] :floor_ut (UMAX) uprated floor Usi-factor target
# @option argh [#to_s] :wall_option ("") construction to uprate (or "all")
# @option argh [#to_s] :roof_option ("") construction to uprate (or "all")
# @option argh [#to_s] :floor_option ("") construction to uprate (or "all")
@@ -172,9 +167,9 @@ def uprate(model = nil, s = {}, argh = {})
argh[:uprate_walls ] = false unless argh.key?(:uprate_walls)
argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs)
argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
- argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut)
- argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut)
- argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut)
+ argh[:wall_ut ] = UMAX unless argh.key?(:wall_ut)
+ argh[:roof_ut ] = UMAX unless argh.key?(:roof_ut)
+ argh[:floor_ut ] = UMAX unless argh.key?(:floor_ut)
argh[:wall_option ] = "" unless argh.key?(:wall_option)
argh[:roof_option ] = "" unless argh.key?(:roof_option)
argh[:floor_option ] = "" unless argh.key?(:floor_option)
@@ -197,7 +192,7 @@ def uprate(model = nil, s = {}, argh = {})
groups.each do |type, g|
next unless g[:up]
next unless g[:ut].is_a?(Numeric)
- next unless g[:ut] < 5.678
+ next unless g[:ut] < UMAX
next if g[:ut] < 0
typ = type
@@ -211,7 +206,7 @@ def uprate(model = nil, s = {}, argh = {})
all = tout.include?(op)
if g[:op].empty?
- log(ERR, "Construction (#{type}) to uprate? (#{mth})")
+ log(WRN, "Construction (#{type}) to uprate? (#{mth})")
elsif all
s.each do |nom, surface|
next unless surface.key?(:deratable )
@@ -247,11 +242,11 @@ def uprate(model = nil, s = {}, argh = {})
else
id = g[:op]
lc = model.getConstructionByName(id)
- log(ERR, "Construction '#{id}'? (#{mth})") if lc.empty?
+ log(WRN, "Construction '#{id}'? (#{mth})") if lc.empty?
next if lc.empty?
lc = lc.get.to_LayeredConstruction
- log(ERR, "'#{id}' layered construction? (#{mth})") if lc.empty?
+ log(WRN, "'#{id}' layered construction? (#{mth})") if lc.empty?
next if lc.empty?
lc = lc.get
@@ -282,7 +277,7 @@ def uprate(model = nil, s = {}, argh = {})
end
if coll.empty?
- log(ERR, "No #{type} construction to uprate - skipping (#{mth})")
+ log(WRN, "No #{type} construction to uprate - skipping (#{mth})")
next
elsif lc
# Valid layered construction - good to uprate!
@@ -291,7 +286,7 @@ def uprate(model = nil, s = {}, argh = {})
lyr[:index] = nil unless lyr[:index] >= 0
lyr[:index] = nil unless lyr[:index] < lc.layers.size
- log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
+ log(WRN, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
next unless lyr[:index]
# Ensure lc is exclusively linked to deratable surfaces of right type.
@@ -302,10 +297,10 @@ def uprate(model = nil, s = {}, argh = {})
next unless surface.key?(:construction)
next unless surface[:construction].is_a?(cl3)
next unless surface[:construction] == lc
+ next unless surface[:deratable]
ok = true
- ok = false unless surface[:type ] == typ
- ok = false unless surface[:deratable]
+ ok = false unless surface[:type] == typ
ok = false unless coll.key?(id)
ok = false unless coll[id][:s].key?(nom)
@@ -346,13 +341,18 @@ def uprate(model = nil, s = {}, argh = {})
if sss.isConstructionDefaulted
set = defaultConstructionSet(sss) # building? story?
- constructions = set.defaultExteriorSurfaceConstructions
- unless constructions.empty?
- constructions = constructions.get
- constructions.setWallConstruction(lc) if typ == :wall
- constructions.setFloorConstruction(lc) if typ == :floor
- constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
+ if set.nil?
+ sss.setConstruction(lc)
+ else
+ constructions = set.defaultExteriorSurfaceConstructions
+
+ unless constructions.empty?
+ constructions = constructions.get
+ constructions.setWallConstruction(lc) if typ == :wall
+ constructions.setFloorConstruction(lc) if typ == :floor
+ constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
+ end
end
else
sss.setConstruction(lc)
@@ -385,7 +385,7 @@ def uprate(model = nil, s = {}, argh = {})
res = uo(model, lc, id, hloss, film, g[:ut])
unless res[:uo] && res[:m]
- log(ERR, "Unable to uprate '#{id}' (#{mth})")
+ log(WRN, "Unable to uprate '#{id}' (#{mth})")
next
end
@@ -407,7 +407,7 @@ def uprate(model = nil, s = {}, argh = {})
argh[:roof_uo ] = res[:uo] if typ == :ceiling
argh[:floor_uo] = res[:uo] if typ == :floor
else
- log(ERR, "Nilled construction to uprate - (#{mth})")
+ log(WRN, "Nilled construction to uprate - (#{mth})")
return false
end
end
@@ -589,7 +589,7 @@ def ua_summary(date = Time.now, argh = {})
empty = shorts[:has].empty? && shorts[:val].empty?
has = shorts[:has] unless empty
val = shorts[:val] unless empty
- log(ERR, "Invalid UA' reference set (#{mth})") if empty
+ log(WRN, "Invalid UA' reference set (#{mth})") if empty
unless empty
ua[:model] += " : Design vs '#{ref}'"
@@ -1000,7 +1000,7 @@ def ua_md(ua = {}, lang = :en)
model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
model += " (v#{ua[:version]})" if ua.key?(:version)
report << model unless model.empty?
- report << "* TBD : v3.4.5"
+ report << "* TBD : v3.5.0"
report << "* date : #{ua[:date]}"
if lang == :en
diff --git a/lib/measures/tbd/resources/utils.rb b/lib/measures/tbd/resources/utils.rb
index 35cb90c..77d8014 100644
--- a/lib/measures/tbd/resources/utils.rb
+++ b/lib/measures/tbd/resources/utils.rb
@@ -31,20 +31,26 @@
require "openstudio"
module OSut
- # DEBUG for devs; WARN/ERROR for users (bad OS input), see OSlg
extend OSlg
- TOL = 0.01 # default distance tolerance (m)
- TOL2 = TOL * TOL # default area tolerance (m2)
- DBG = OSlg::DEBUG.dup # see github.com/rd2/oslg
- INF = OSlg::INFO.dup # see github.com/rd2/oslg
- WRN = OSlg::WARN.dup # see github.com/rd2/oslg
- ERR = OSlg::ERROR.dup # see github.com/rd2/oslg
- FTL = OSlg::FATAL.dup # see github.com/rd2/oslg
- NS = "nameString" # OpenStudio object identifier method
-
- HEAD = 2.032 # standard 80" door
- SILL = 0.762 # standard 30" window sill
+ DBG = OSlg::DEBUG.dup # see github.com/rd2/oslg
+ INF = OSlg::INFO.dup # see github.com/rd2/oslg
+ WRN = OSlg::WARN.dup # see github.com/rd2/oslg
+ ERR = OSlg::ERROR.dup # see github.com/rd2/oslg
+ FTL = OSlg::FATAL.dup # see github.com/rd2/oslg
+ NS = "nameString" # OpenStudio object identifier method
+ TOL = 0.01 # default distance tolerance (m)
+ TOL2 = TOL * TOL # default area tolerance (m2)
+ HEAD = 2.032 # standard 80" door
+ SILL = 0.762 # standard 30" window sill
+ DMIN = 0.010 # min. insulating material thickness
+ DMAX = 1.000 # max. insulating material thickness
+ KMIN = 0.010 # min. insulating material thermal conductivity
+ KMAX = 2.000 # max. insulating material thermal conductivity
+ UMAX = KMAX / DMIN # material USi upper limit, 200.000
+ UMIN = KMIN / DMAX # material USi lower limit, 0.010
+ RMIN = 1.0 / UMAX # material RSi lower limit, 0.005 (or R-IP 0.03)
+ RMAX = 1.0 / UMIN # material RSi upper limit, 100.000 (or R-IP 567.80)
# General surface orientations (see facets method)
SIDZ = [:bottom, # e.g. ground-facing, exposed floors
@@ -191,6 +197,388 @@ module OSut
@@mats[:door ][:rho] = 600.000
@@mats[:door ][:cp ] = 1000.000
+ ##
+ # Validates if every material in a layered construction is standard & opaque.
+ #
+ # @param lc [OpenStudio::LayeredConstruction] a layered construction
+ #
+ # @return [Bool] whether all layers are valid
+ # @return [false] if invalid input (see logs)
+ def standardOpaqueLayers?(lc = nil)
+ mth = "OSut::#{__callee__}"
+ cl = OpenStudio::Model::LayeredConstruction
+ return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
+ return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
+
+ lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
+
+ true
+ end
+
+ ##
+ # Returns total (standard opaque) layered construction thickness (m).
+ #
+ # @param lc [OpenStudio::LayeredConstruction] a layered construction
+ #
+ # @return [Float] construction thickness
+ # @return [0.0] if invalid input (see logs)
+ def thickness(lc = nil)
+ mth = "OSut::#{__callee__}"
+ cl = OpenStudio::Model::LayeredConstruction
+ return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
+ return mismatch(lc.nameString, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
+
+ unless standardOpaqueLayers?(lc)
+ log(ERR, "#{lc.nameString} holds non-StandardOpaqueMaterial(s) (#{mth})")
+ return 0.0
+ end
+
+ thickness = 0.0
+
+ lc.layers.each { |m| thickness += m.thickness }
+
+ thickness
+ end
+
+ ##
+ # Returns total air film resistance of a fenestrated construction (m2•K/W)
+ #
+ # @param usi [Numeric] a fenestrated construction's U-factor (W/m2•K)
+ #
+ # @return [Float] total air film resistances
+ # @return [0.1216] if invalid input (see logs)
+ def glazingAirFilmRSi(usi = 5.85)
+ # The sum of thermal resistances of calculated exterior and interior film
+ # coefficients under standard winter conditions are taken from:
+ #
+ # https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
+ # window-calculation-module.html#simple-window-model
+ #
+ # These remain acceptable approximations for flat windows, yet likely
+ # unsuitable for subsurfaces with curved or projecting shapes like domed
+ # skylights. The solution here is considered an adequate fix for reporting.
+ #
+ # For U-factors above 8.0 W/m2•K (or invalid input), the function returns
+ # 0.1216 m2•K/W, which corresponds to a construction with a single glass
+ # layer thickness of 2mm & k = ~0.6 W/m.K.
+ #
+ # The EnergyPlus Engineering calculations were designed for vertical
+ # windows - not horizontal, slanted or domed surfaces - use with caution.
+ mth = "OSut::#{__callee__}"
+ cl = Numeric
+ return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl)
+ return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
+ return negative("usi", mth, WRN, 0.1216) if usi < 0
+ return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL
+
+ rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
+ return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
+ return rsi + 1 / (1.788041 * usi - 2.886625)
+ end
+
+ ##
+ # Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
+ # includes air film resistances. It excludes insulating effects of shades,
+ # screens, etc. in the case of fenestrated constructions.
+ #
+ # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
+ # @param film [Numeric] thermal resistance of surface air films (m2•K/W)
+ # @param t [Numeric] gas temperature (°C) (optional)
+ #
+ # @return [Float] layered construction's thermal resistance
+ # @return [0.0] if invalid input (see logs)
+ def rsi(lc = nil, film = 0.0, t = 0.0)
+ # This is adapted from BTAP's Material Module "get_conductance" (P. Lopez)
+ #
+ # https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
+ # c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
+ # btap_equest_converter/envelope.rb#L122
+ mth = "OSut::#{__callee__}"
+ cl1 = OpenStudio::Model::LayeredConstruction
+ cl2 = Numeric
+ return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
+ return mismatch(lc.nameString, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
+ return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
+ return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
+
+ t += 273.0 # °C to K
+ return negative("temp K", mth, ERR, 0.0) if t < 0
+ return negative("film", mth, ERR, 0.0) if film < 0
+
+ rsi = film
+
+ lc.layers.each do |m|
+ # Fenestration materials first.
+ empty = m.to_SimpleGlazing.empty?
+ return 1 / m.to_SimpleGlazing.get.uFactor unless empty
+
+ empty = m.to_StandardGlazing.empty?
+ rsi += m.to_StandardGlazing.get.thermalResistance unless empty
+ empty = m.to_RefractionExtinctionGlazing.empty?
+ rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
+ empty = m.to_Gas.empty?
+ rsi += m.to_Gas.get.getThermalResistance(t) unless empty
+ empty = m.to_GasMixture.empty?
+ rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
+
+ # Opaque materials next.
+ empty = m.to_StandardOpaqueMaterial.empty?
+ rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty
+ empty = m.to_MasslessOpaqueMaterial.empty?
+ rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty
+ empty = m.to_RoofVegetation.empty?
+ rsi += m.to_RoofVegetation.get.thermalResistance unless empty
+ empty = m.to_AirGap.empty?
+ rsi += m.to_AirGap.get.thermalResistance unless empty
+ end
+
+ rsi
+ end
+
+ ##
+ # Identifies a layered construction's (opaque) insulating layer. The method
+ # returns a 3-keyed hash :index, the insulating layer index [0, n layers)
+ # within the layered construction; :type, either :standard or :massless; and
+ # :r, material thermal resistance in m2•K/W.
+ #
+ # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
+ #
+ # @return [Hash] index: (Integer), type: (Symbol), r: (Float)
+ # @return [Hash] index: nil, type: nil, r: 0.0 if invalid input (see logs)
+ def insulatingLayer(lc = nil)
+ mth = "OSut::#{__callee__}"
+ cl = OpenStudio::Model::LayeredConstruction
+ res = { index: nil, type: nil, r: 0.0 }
+ i = 0 # iterator
+ return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
+ return mismatch(lc.nameString, lc, cl, mth, DBG, res) unless lc.is_a?(cl)
+
+ lc.layers.each do |m|
+ unless m.to_MasslessOpaqueMaterial.empty?
+ m = m.to_MasslessOpaqueMaterial.get
+
+ if m.thermalResistance < RMIN || m.thermalResistance < res[:r]
+ i += 1
+ next
+ else
+ res[:r ] = m.thermalResistance
+ res[:index] = i
+ res[:type ] = :massless
+ end
+ end
+
+ unless m.to_StandardOpaqueMaterial.empty?
+ m = m.to_StandardOpaqueMaterial.get
+ k = m.thermalConductivity
+ d = m.thickness
+
+ if d < DMIN || k > KMAX || d / k < res[:r]
+ i += 1
+ next
+ else
+ res[:r ] = d / k
+ res[:index] = i
+ res[:type ] = :standard
+ end
+ end
+
+ i += 1
+ end
+
+ res
+ end
+
+ ##
+ # Validates whether a material is both uniquely reserved to a single layered
+ # construction in a model, and referenced only once in the construction.
+ # Limited to 'standard' or 'massless' materials.
+ #
+ # @param m [OpenStudio::Model::OpaqueMaterial] a material
+ #
+ # @return [Boolean] whether material is unique
+ # @return [false] if missing)
+ def uniqueMaterial?(m = nil)
+ mth = "OSut::#{__callee__}"
+ cl1 = OpenStudio::Model::OpaqueMaterial
+ return invalid("mat", mth, 1, DBG, false) unless m.respond_to?(NS)
+ return mismatch(m.nameString, m, cl1, mth, DBG, false) unless m.is_a?(cl1)
+
+ num = 0
+ lcs = m.model.getLayeredConstructions
+
+ unless m.to_MasslessOpaqueMaterial.empty?
+ m = m.to_MasslessOpaqueMaterial.get
+
+ lcs.each { |lc| num += lc.getLayerIndices(m).size }
+
+ return true if num == 1
+ end
+
+ unless m.to_StandardOpaqueMaterial.empty?
+ m = m.to_StandardOpaqueMaterial.get
+
+ lcs.each { |lc| num += lc.getLayerIndices(m).size }
+
+ return true if num == 1
+ end
+
+ false
+ end
+
+ ##
+ # Sets a layered construction material as unique. Solution similar to
+ # OpenStudio::Model::LayeredConstruction's 'ensureUniqueLayers', yet limited
+ # here to a single indexed OpenStudio material, typically the principal
+ # insulating material. Returns true if the indexed material is already unique.
+ # Limited to 'standard' or 'massless' materials.
+ #
+ # @param lc [OpenStudio::Model::LayeredConstruction] a construction
+ # @param index [Integer] the construction layer index of the material
+ #
+ # @return [Boolean] if assigned as unique
+ # @return [false] if invalid inputs
+ def assignUniqueMaterial(lc = nil, index = nil)
+ mth = "OSut::#{__callee__}"
+ cl1 = OpenStudio::Model::LayeredConstruction
+ cl2 = Integer
+ return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
+ return mismatch(lc.nameString, lc, cl1, mth, DBG, false) unless lc.is_a?(cl1)
+ return mismatch("index", index, cl2, mth, DBG, false) unless index.is_a?(cl2)
+ return invalid("index", mth, 0, DBG, false) unless index.between?(0, lc.numLayers - 1)
+
+ m = lc.getLayer(index)
+
+ unless m.to_MasslessOpaqueMaterial.empty?
+ m = m.to_MasslessOpaqueMaterial.get
+ return true if uniqueMaterial?(m)
+
+ mat = m.clone(m.model).to_MasslessOpaqueMaterial.get
+ return lc.setLayer(index, mat)
+ end
+
+ unless m.to_StandardOpaqueMaterial.empty?
+ m = m.to_StandardOpaqueMaterial.get
+ return true if uniqueMaterial?(m)
+
+ mat = m.clone(m.model).to_StandardOpaqueMaterial.get
+ return lc.setLayer(index, mat)
+ end
+
+ false
+ end
+
+ ##
+ # Resets a construction's Uo factor by adjusting its insulating layer
+ # thermal conductivity, then if needed its thickness (or its RSi value if
+ # massless). Unless material uniquness is requested, a matching material is
+ # recovered instead of instantiating a new one. The latter is renamed
+ # according to its adjusted conductivity/thickness (or RSi value).
+ #
+ # @param lc [OpenStudio::Model::LayeredConstruction] a construction
+ # @param film [Float] construction air film resistance
+ # @param index [Integer] the insulating layer's array index
+ # @param uo [Float] desired Uo factor (with air film resistance)
+ # @param uniq [Boolean] whether to enforce material uniqueness
+ #
+ # @return [Float] new layer RSi [RMIN, RMAX]
+ # @return [0.0] if invalid input
+ def resetUo(lc = nil, film = nil, index = nil, uo = nil, uniq = false)
+ mth = "OSut::#{__callee__}"
+ r = 0.0 # thermal resistance of new material
+ cl1 = OpenStudio::Model::LayeredConstruction
+ cl2 = Numeric
+ cl3 = Integer
+ return invalid("lc", mth, 1, DBG, r) unless lc.respond_to?(NS)
+ return mismatch(lc.nameString, lc, cl1, mth, DBG, r) unless lc.is_a?(cl1)
+ return mismatch("film", film, cl2, mth, DBG, r) unless film.is_a?(cl2)
+ return negative("film", mth, DBG, r) if film.negative?
+ return mismatch("index", index, cl3, mth, DBG, r) unless index.is_a?(cl3)
+ return invalid("index", mth, 3, DBG, r) unless index.between?(0, lc.numLayers - 1)
+ return mismatch("uo", uo, cl2, mth, DBG, r) unless uo.is_a?(cl2)
+
+ unless uo.between?(UMIN, UMAX)
+ uo = clamp(UMIN, UMAX)
+ log(WRN, "Resetting Uo (#{lc.nameString}) to #{uo.round(3)} (#{mth})")
+ end
+
+ uniq = false unless [true, false].include?(uniq)
+ r0 = rsi(lc, film) # current construction RSi value
+ ro = 1 / uo # desired construction RSi value
+ dR = ro - r0 # desired increase in construction RSi
+ m = lc.getLayer(index)
+
+ unless m.to_MasslessOpaqueMaterial.empty?
+ m = m.to_MasslessOpaqueMaterial.get
+ r = m.thermalResistance
+ return r if dR.abs.round(2) == 0.00
+
+ r = (r + dR).clamp(RMIN, RMAX)
+ id = "OSut:RSi#{r.round(2)}"
+ mt = lc.model.getMasslessOpaqueMaterialByName(id)
+
+ # Existing material?
+ unless mt.empty?
+ mt = mt.get
+
+ if r.round(2) == mt.thermalResistance.round(2) && uniq == false
+ lc.setLayer(index, mt)
+ return r
+ end
+ end
+
+ mt = m.clone(m.model).to_MasslessOpaqueMaterial.get
+ mt.setName(id)
+
+ unless mt.setThermalResistance(r)
+ return invalid("Failed #{id}: RSi#{de_r.round(2)}", mth)
+ end
+
+ lc.setLayer(index, mt)
+
+ return r
+ end
+
+ unless m.to_StandardOpaqueMaterial.empty?
+ m = m.to_StandardOpaqueMaterial.get
+ r = m.thickness / m.conductivity
+ return r if dR.abs.round(2) == 0.00
+
+ k = (m.thickness / (r + dR)).clamp(KMIN, KMAX)
+ d = (k * (r + dR)).clamp(DMIN, DMAX)
+ r = d / k
+ id = "OSUT:K#{format('%4.3f', k)}:#{format('%03d', d*1000)[-3..-1]}"
+ mt = lc.model.getStandardOpaqueMaterialByName(id)
+
+ # Existing material?
+ unless mt.empty?
+ mt = mt.get
+ rt = mt.thickness / mt.conductivity
+
+ if r.round(2) == rt.round(2) && uniq == false
+ lc.setLayer(index, mt)
+ return r
+ end
+ end
+
+ mt = m.clone(m.model).to_StandardOpaqueMaterial.get
+ mt.setName(id)
+
+ unless mt.setThermalConductivity(k)
+ return invalid("Failed #{id}: K#{k.round(3)}", mth)
+ end
+
+ unless mt.setThickness(d)
+ return invalid("Failed #{id}: #{(d*1000).to_i}mm", mth)
+ end
+
+ lc.setLayer(index, mt)
+
+ return r
+ end
+
+ 0
+ end
+
##
# Generates an OpenStudio multilayered construction, + materials if needed.
#
@@ -214,20 +602,27 @@ def genConstruction(model = nil, specs = {})
specs[:id] = "" unless specs.key?(:id)
id = trim(specs[:id])
- id = "OSut|CON|#{specs[:type]}" if id.empty?
+ id = "OSut:CON:#{specs[:type]}" if id.empty?
- specs[:type] = :wall unless specs.key?(:type)
- chk = @@uo.keys.include?(specs[:type])
- return invalid("surface type", mth, 2, ERR) unless chk
+ if specs.key?(:type)
+ unless @@uo.keys.include?(specs[:type])
+ return invalid("surface type", mth, 2, ERR)
+ end
+ else
+ specs[:type] = :wall
+ end
specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo) # can be nil
u = specs[:uo]
- if u
- return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric)
- return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678
- return zero("#{id} Uo", mth, ERR) if u.round(2) == 0.00
- return negative("#{id} Uo", mth, ERR) if u < 0
+ unless u.nil?
+ return mismatch("#{id} Uo", u, Numeric, mth) unless u.is_a?(Numeric)
+
+ unless u.between?(UMIN, 5.678)
+ uO = u
+ u = uO.clamp(UMIN, 5.678)
+ log(ERR, "Resetting Uo #{uO.round(3)} to #{u.round(3)} (#{mth})")
+ end
end
# Optional specs. Log/reset if invalid.
@@ -256,14 +651,14 @@ def genConstruction(model = nil, specs = {})
d = 0.015
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
when :partition
unless specs[:clad] == :none
d = 0.015
mt = :drywall
a[:clad][:mat] = @@mats[mt]
a[:clad][:d ] = d
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
d = 0.015
@@ -275,14 +670,14 @@ def genConstruction(model = nil, specs = {})
mt = :mineral if u
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
unless specs[:finish] == :none
d = 0.015
mt = :drywall
a[:finish][:mat] = @@mats[mt]
a[:finish][:d ] = d
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
when :wall
unless specs[:clad] == :none
@@ -293,7 +688,7 @@ def genConstruction(model = nil, specs = {})
d = 0.015 if specs[:clad] == :light
a[:clad][:mat] = @@mats[mt]
a[:clad][:d ] = d
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
mt = :drywall
@@ -303,7 +698,7 @@ def genConstruction(model = nil, specs = {})
d = 0.015 if specs[:frame] == :light
a[:sheath][:mat] = @@mats[mt]
a[:sheath][:d ] = d
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
mt = :mineral
mt = :cellulose if specs[:frame] == :medium
@@ -315,7 +710,7 @@ def genConstruction(model = nil, specs = {})
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
unless specs[:finish] == :none
mt = :concrete
@@ -325,7 +720,7 @@ def genConstruction(model = nil, specs = {})
d = 0.200 if specs[:finish] == :heavy
a[:finish][:mat] = @@mats[mt]
a[:finish][:d ] = d
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
when :roof
unless specs[:clad] == :none
@@ -336,7 +731,7 @@ def genConstruction(model = nil, specs = {})
d = 0.200 if specs[:clad] == :heavy # e.g. parking garage
a[:clad][:mat] = @@mats[mt]
a[:clad][:d ] = d
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
mt = :mineral
@@ -347,7 +742,7 @@ def genConstruction(model = nil, specs = {})
d = 0.015 unless u
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
unless specs[:finish] == :none
mt = :concrete
@@ -357,7 +752,7 @@ def genConstruction(model = nil, specs = {})
d = 0.200 if specs[:finish] == :heavy
a[:finish][:mat] = @@mats[mt]
a[:finish][:d ] = d
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
when :floor
unless specs[:clad] == :none
@@ -365,7 +760,7 @@ def genConstruction(model = nil, specs = {})
d = 0.015
a[:clad][:mat] = @@mats[mt]
a[:clad][:d ] = d
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
mt = :mineral
@@ -376,7 +771,7 @@ def genConstruction(model = nil, specs = {})
d = 0.015 unless u
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
unless specs[:finish] == :none
mt = :concrete
@@ -386,21 +781,21 @@ def genConstruction(model = nil, specs = {})
d = 0.200 if specs[:finish] == :heavy
a[:finish][:mat] = @@mats[mt]
a[:finish][:d ] = d
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
when :slab
mt = :sand
d = 0.100
a[:clad][:mat] = @@mats[mt]
a[:clad][:d ] = d
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
unless specs[:frame] == :none
mt = :polyiso
d = 0.025
a[:sheath][:mat] = @@mats[mt]
a[:sheath][:d ] = d
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
mt = :concrete
@@ -408,14 +803,14 @@ def genConstruction(model = nil, specs = {})
d = 0.200 if specs[:frame] == :heavy
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
unless specs[:finish] == :none
mt = :material
d = 0.015
a[:finish][:mat] = @@mats[mt]
a[:finish][:d ] = d
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
when :basement
unless specs[:clad] == :none
@@ -425,38 +820,38 @@ def genConstruction(model = nil, specs = {})
d = 0.015 if specs[:clad] == :light
a[:clad][:mat] = @@mats[mt]
a[:clad][:d ] = d
- a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:clad][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
mt = :polyiso
d = 0.025
a[:sheath][:mat] = @@mats[mt]
a[:sheath][:d ] = d
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
mt = :concrete
d = 0.200
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
else
mt = :concrete
d = 0.200
a[:sheath][:mat] = @@mats[mt]
a[:sheath][:d ] = d
- a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:sheath][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
unless specs[:finish] == :none
mt = :mineral
d = 0.075
a[:compo][:mat] = @@mats[mt]
a[:compo][:d ] = d
- a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
mt = :drywall
d = 0.015
a[:finish][:mat] = @@mats[mt]
a[:finish][:d ] = d
- a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:finish][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
end
end
when :door
@@ -465,27 +860,25 @@ def genConstruction(model = nil, specs = {})
a[:compo ][:mat ] = @@mats[mt]
a[:compo ][:d ] = d
- a[:compo ][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
+ a[:compo ][:id ] = "OSut:#{mt}:#{format('%03d', d*1000)[-3..-1]}"
when :window
a[:glazing][:u ] = u ? u : @@uo[:window]
a[:glazing][:shgc] = 0.450
a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
- a[:glazing][:id ] = "OSut|window"
- a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
- a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
+ a[:glazing][:id ] = "OSut:window"
+ a[:glazing][:id ] += ":U#{format('%.1f', a[:glazing][:u])}"
+ a[:glazing][:id ] += ":SHGC#{format('%d', a[:glazing][:shgc]*100)}"
when :skylight
a[:glazing][:u ] = u ? u : @@uo[:skylight]
a[:glazing][:shgc] = 0.450
a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
- a[:glazing][:id ] = "OSut|skylight"
- a[:glazing][:id ] += "|U#{format('%.1f', a[:glazing][:u])}"
- a[:glazing][:id ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
+ a[:glazing][:id ] = "OSut:skylight"
+ a[:glazing][:id ] += ":U#{format('%.1f', a[:glazing][:u])}"
+ a[:glazing][:id ] += ":SHGC#{format('%d', a[:glazing][:shgc]*100)}"
end
# Initiate layers.
- unglazed = a[:glazing].empty? ? true : false
-
- if unglazed
+ if a[:glazing].empty?
layers = OpenStudio::Model::OpaqueMaterialVector.new
# Loop through each layer spec, and generate construction.
@@ -528,14 +921,15 @@ def genConstruction(model = nil, specs = {})
layers << lyr
end
- c = OpenStudio::Model::Construction.new(layers)
+ c = OpenStudio::Model::Construction.new(layers)
c.setName(id)
- # Adjust insulating layer thickness or conductivity to match requested Uo.
- if u and unglazed
+ # Adjust insulating layer conductivity (maybe thickness) to match Uo.
+ if u and a[:glazing].empty?
ro = 1 / u - film
- if ro > 0
+
+ if ro > RMIN
if specs[:type] == :door # 1x layer, adjust conductivity
layer = c.getLayer(0).to_StandardOpaqueMaterial
return invalid("#{id} standard material?", mth, 0) if layer.empty?
@@ -543,34 +937,33 @@ def genConstruction(model = nil, specs = {})
layer = layer.get
k = layer.thickness / ro
layer.setConductivity(k)
- else # multiple layers, adjust insulating layer thickness
+ else # multiple layers, adjust layer conductivity, then thickness
lyr = insulatingLayer(c)
return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
- return invalid("#{id} construction", mth, 0) if lyr[:r ].zero?
+ return invalid("#{id} construction", mth, 0) if lyr[:r ].to_i.zero?
index = lyr[:index]
layer = c.getLayer(index).to_StandardOpaqueMaterial
return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
layer = layer.get
- k = layer.conductivity
- d = (ro - rsi(c) + lyr[:r]) * k
- return invalid("#{id} adjusted m", mth, 0) if d < 0.03
- nom = "OSut|"
- nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
- nom += "|"
- nom += format("%03d", d*1000)[-3..-1]
+ k = (layer.thickness / (ro - rsi(c) + lyr[:r])).clamp(KMIN, KMAX)
+ d = (k * (ro - rsi(c) + lyr[:r])).clamp(DMIN, DMAX)
+
+ nom = "OSut:"
+ nom += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
+ nom += ":K#{format('%4.3f', k)}:#{format('%03d', d*1000)[-3..-1]}"
lyr = model.getStandardOpaqueMaterialByName(nom)
if lyr.empty?
layer.setName(nom)
+ layer.setConductivity(k)
layer.setThickness(d)
else
- omat = lyr.get
- c.setLayer(index, omat)
+ c.setLayer(index, lyr.get)
end
end
end
@@ -619,20 +1012,20 @@ def genShade(subs = OpenStudio::Model::SubSurfaceVector.new)
end
# Shading schedule.
- id = "OSut|SHADE|Ruleset"
+ id = "OSut:SHADE:Ruleset"
sch = mdl.getScheduleRulesetByName(id)
if sch.empty?
sch = OpenStudio::Model::ScheduleRuleset.new(mdl, 0)
sch.setName(id)
sch.setScheduleTypeLimits(onoff)
- sch.defaultDaySchedule.setName("OSut|Shade|Ruleset|Default")
+ sch.defaultDaySchedule.setName("OSut:Shade:Ruleset:Default")
else
sch = sch.get
end
# Summer cooling rule.
- id = "OSut|SHADE|ScheduleRule"
+ id = "OSut:SHADE:ScheduleRule"
rule = mdl.getScheduleRuleByName(id)
if rule.empty?
@@ -646,14 +1039,14 @@ def genShade(subs = OpenStudio::Model::SubSurfaceVector.new)
rule.setStartDate(start)
rule.setEndDate(finish)
rule.setApplyAllDays(true)
- rule.daySchedule.setName("OSut|Shade|Rule|Default")
+ rule.daySchedule.setName("OSut:Shade:Rule:Default")
rule.daySchedule.addValue(OpenStudio::Time.new(0,24,0,0), 1)
else
rule = rule.get
end
# Shade object.
- id = "OSut|Shade"
+ id = "OSut:Shade"
shd = mdl.getShadeByName(id)
if shd.empty?
@@ -664,7 +1057,7 @@ def genShade(subs = OpenStudio::Model::SubSurfaceVector.new)
end
# Shading control (unique to each call).
- id = "OSut|ShadingControl"
+ id = "OSut:ShadingControl"
ctl = OpenStudio::Model::ShadingControl.new(shd)
ctl.setName(id)
ctl.setSchedule(sch)
@@ -700,7 +1093,7 @@ def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0)
# A single material.
mdl = sps.first.model
- id = "OSut|MASS|Material"
+ id = "OSut:MASS:Material"
mat = mdl.getOpaqueMaterialByName(id)
if mat.empty?
@@ -719,7 +1112,7 @@ def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0)
end
# A single, 1x layered construction.
- id = "OSut|MASS|Construction"
+ id = "OSut:MASS:Construction"
con = mdl.getConstructionByName(id)
if con.empty?
@@ -732,7 +1125,7 @@ def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0)
con = con.get
end
- id = "OSut|InternalMassDefinition|" + (format "%.2f", ratio)
+ id = "OSut:InternalMassDefinition:" + (format "%.2f", ratio)
df = mdl.getInternalMassDefinitionByName(id)
if df.empty?
@@ -746,7 +1139,7 @@ def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0)
sps.each do |sp|
mass = OpenStudio::Model::InternalMass.new(df)
- mass.setName("OSut|InternalMass|#{sp.nameString}")
+ mass.setName("OSut:InternalMass:#{sp.nameString}")
mass.setSpace(sp)
end
@@ -754,13 +1147,13 @@ def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0)
end
##
- # Validates if a default construction set holds a base construction.
+ # Validates if a default construction set holds an opaque base construction.
#
# @param set [OpenStudio::Model::DefaultConstructionSet] a default set
- # @param bse [OpenStudio::Model::ConstructionBase] a construction base
+ # @param bse [OpenStudio::Model::ConstructionBase] an opaque construction base
# @param gr [Bool] if ground-facing surface
# @param ex [Bool] if exterior-facing surface
- # @param tp [#to_s] a surface type
+ # @param tp [#to_sym] surface type: "floor", "wall" or "roofceiling"
#
# @return [Bool] whether default set holds construction
# @return [false] if invalid input (see logs)
@@ -779,7 +1172,7 @@ def holdsConstruction?(set = nil, bse = nil, gr = false, ex = false, tp = "")
ck2 = bse.is_a?(cl2)
ck3 = [true, false].include?(gr)
ck4 = [true, false].include?(ex)
- ck5 = tp.respond_to?(:to_s)
+ ck5 = tp.respond_to?(:to_sym)
return mismatch(id1, set, cl1, mth, DBG, false) unless ck1
return mismatch(id2, bse, cl2, mth, DBG, false) unless ck2
return invalid("ground" , mth, 3, DBG, false) unless ck3
@@ -859,6 +1252,9 @@ def defaultConstructionSet(s = nil)
type = s.surfaceType
ground = false
exterior = false
+ adjacent = s.adjacentSurface.empty? ? nil : s.adjacentSurface.get
+ aspace = adjacent.nil? || adjacent.space.empty? ? nil : adjacent.space.get
+ typ = adjacent.nil? ? nil : adjacent.surfaceType
if s.isGroundSurface
ground = true
@@ -866,7 +1262,14 @@ def defaultConstructionSet(s = nil)
exterior = true
end
- unless space.defaultConstructionSet.empty?
+ if space.defaultConstructionSet.empty?
+ unless aspace.nil?
+ unless aspace.defaultConstructionSet.empty?
+ set = aspace.defaultConstructionSet.get
+ return set if holdsConstruction?(set, base, ground, exterior, typ)
+ end
+ end
+ else
set = space.defaultConstructionSet.get
return set if holdsConstruction?(set, base, ground, exterior, type)
end
@@ -880,6 +1283,17 @@ def defaultConstructionSet(s = nil)
end
end
+ unless aspace.nil? || aspace.spaceType.empty?
+ unless aspace.spaceType.empty?
+ spacetype = aspace.spaceType.get
+
+ unless spacetype.defaultConstructionSet.empty?
+ set = spacetype.defaultConstructionSet.get
+ return set if holdsConstruction?(set, base, ground, exterior, typ)
+ end
+ end
+ end
+
unless space.buildingStory.empty?
story = space.buildingStory.get
@@ -889,6 +1303,15 @@ def defaultConstructionSet(s = nil)
end
end
+ unless aspace.nil? || aspace.buildingStory.empty?
+ story = aspace.buildingStory.get
+
+ unless spacetype.defaultConstructionSet.empty?
+ set = spacetype.defaultConstructionSet.get
+ return set if holdsConstruction?(set, base, ground, exterior, typ)
+ end
+ end
+
building = mdl.getBuilding
unless building.defaultConstructionSet.empty?
@@ -899,203 +1322,6 @@ def defaultConstructionSet(s = nil)
nil
end
- ##
- # Validates if every material in a layered construction is standard & opaque.
- #
- # @param lc [OpenStudio::LayeredConstruction] a layered construction
- #
- # @return [Bool] whether all layers are valid
- # @return [false] if invalid input (see logs)
- def standardOpaqueLayers?(lc = nil)
- mth = "OSut::#{__callee__}"
- cl = OpenStudio::Model::LayeredConstruction
- return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
- return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
-
- lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
-
- true
- end
-
- ##
- # Returns total (standard opaque) layered construction thickness (m).
- #
- # @param lc [OpenStudio::LayeredConstruction] a layered construction
- #
- # @return [Float] construction thickness
- # @return [0.0] if invalid input (see logs)
- def thickness(lc = nil)
- mth = "OSut::#{__callee__}"
- cl = OpenStudio::Model::LayeredConstruction
- return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
-
- id = lc.nameString
- return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
-
- ok = standardOpaqueLayers?(lc)
- log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok
- return 0.0 unless ok
-
- thickness = 0.0
- lc.layers.each { |m| thickness += m.thickness }
-
- thickness
- end
-
- ##
- # Returns total air film resistance of a fenestrated construction (m2•K/W)
- #
- # @param usi [Numeric] a fenestrated construction's U-factor (W/m2•K)
- #
- # @return [Float] total air film resistances
- # @return [0.1216] if invalid input (see logs)
- def glazingAirFilmRSi(usi = 5.85)
- # The sum of thermal resistances of calculated exterior and interior film
- # coefficients under standard winter conditions are taken from:
- #
- # https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
- # window-calculation-module.html#simple-window-model
- #
- # These remain acceptable approximations for flat windows, yet likely
- # unsuitable for subsurfaces with curved or projecting shapes like domed
- # skylights. The solution here is considered an adequate fix for reporting,
- # awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
- # (or ISO) air film resistances under standard winter conditions.
- #
- # For U-factors above 8.0 W/m2•K (or invalid input), the function returns
- # 0.1216 m2•K/W, which corresponds to a construction with a single glass
- # layer thickness of 2mm & k = ~0.6 W/m.K.
- #
- # The EnergyPlus Engineering calculations were designed for vertical
- # windows - not horizontal, slanted or domed surfaces - use with caution.
- mth = "OSut::#{__callee__}"
- cl = Numeric
- return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl)
- return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
- return negative("usi", mth, WRN, 0.1216) if usi < 0
- return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL
-
- rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
- return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
- return rsi + 1 / (1.788041 * usi - 2.886625)
- end
-
- ##
- # Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
- # includes air film resistances. It excludes insulating effects of shades,
- # screens, etc. in the case of fenestrated constructions.
- #
- # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
- # @param film [Numeric] thermal resistance of surface air films (m2•K/W)
- # @param t [Numeric] gas temperature (°C) (optional)
- #
- # @return [Float] layered construction's thermal resistance
- # @return [0.0] if invalid input (see logs)
- def rsi(lc = nil, film = 0.0, t = 0.0)
- # This is adapted from BTAP's Material Module "get_conductance" (P. Lopez)
- #
- # https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
- # c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
- # btap_equest_converter/envelope.rb#L122
- mth = "OSut::#{__callee__}"
- cl1 = OpenStudio::Model::LayeredConstruction
- cl2 = Numeric
- return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
-
- id = lc.nameString
- return mismatch(id, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
- return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
- return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
-
- t += 273.0 # °C to K
- return negative("temp K", mth, ERR, 0.0) if t < 0
- return negative("film", mth, ERR, 0.0) if film < 0
-
- rsi = film
-
- lc.layers.each do |m|
- # Fenestration materials first.
- empty = m.to_SimpleGlazing.empty?
- return 1 / m.to_SimpleGlazing.get.uFactor unless empty
-
- empty = m.to_StandardGlazing.empty?
- rsi += m.to_StandardGlazing.get.thermalResistance unless empty
- empty = m.to_RefractionExtinctionGlazing.empty?
- rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
- empty = m.to_Gas.empty?
- rsi += m.to_Gas.get.getThermalResistance(t) unless empty
- empty = m.to_GasMixture.empty?
- rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty
-
- # Opaque materials next.
- empty = m.to_StandardOpaqueMaterial.empty?
- rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty
- empty = m.to_MasslessOpaqueMaterial.empty?
- rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty
- empty = m.to_RoofVegetation.empty?
- rsi += m.to_RoofVegetation.get.thermalResistance unless empty
- empty = m.to_AirGap.empty?
- rsi += m.to_AirGap.get.thermalResistance unless empty
- end
-
- rsi
- end
-
- ##
- # Identifies a layered construction's (opaque) insulating layer. The method
- # returns a 3-keyed hash :index, the insulating layer index [0, n layers)
- # within the layered construction; :type, either :standard or :massless; and
- # :r, material thermal resistance in m2•K/W.
- #
- # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
- #
- # @return [Hash] index: (Integer), type: (Symbol), r: (Float)
- # @return [Hash] index: nil, type: nil, r: 0 if invalid input (see logs)
- def insulatingLayer(lc = nil)
- mth = "OSut::#{__callee__}"
- cl = OpenStudio::Model::LayeredConstruction
- res = { index: nil, type: nil, r: 0.0 }
- i = 0 # iterator
- return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
-
- id = lc.nameString
- return mismatch(id, lc, cl, mth, DBG, res) unless lc.is_a?(cl)
-
- lc.layers.each do |m|
- unless m.to_MasslessOpaqueMaterial.empty?
- m = m.to_MasslessOpaqueMaterial.get
-
- if m.thermalResistance < 0.001 || m.thermalResistance < res[:r]
- i += 1
- next
- else
- res[:r ] = m.thermalResistance
- res[:index] = i
- res[:type ] = :massless
- end
- end
-
- unless m.to_StandardOpaqueMaterial.empty?
- m = m.to_StandardOpaqueMaterial.get
- k = m.thermalConductivity
- d = m.thickness
-
- if d < 0.003 || k > 3.0 || d / k < res[:r]
- i += 1
- next
- else
- res[:r ] = d / k
- res[:index] = i
- res[:type ] = :standard
- end
- end
-
- i += 1
- end
-
- res
- end
-
##
# Validates whether opaque surface can be considered as a curtain wall (or
# similar technology) spandrel, regardless of construction layers, by looking
@@ -2216,7 +2442,7 @@ def availabilitySchedule(model = nil, avl = "")
cl = OpenStudio::Model::Model
limits = nil
return mismatch("model", model, cl, mth) unless model.is_a?(cl)
- return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_s)
+ return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_sym)
# Either fetch availability ScheduleTypeLimits object, or create one.
model.getScheduleTypeLimitss.each do |l|
@@ -4648,7 +4874,7 @@ def spaceWidth(space = nil)
#
# @param s [Set] a (larger) parent set of points
# @param [Array] set a collection of (smaller) sequenced points
- # @option [Symbol] tag sequence of subset vertices to target
+ # @option [#to_sym] tag sequence of subset vertices to target
#
# @return [Integer] number of successfully anchored subsets (see logs)
def genAnchors(s = nil, set = [], tag = :box)
@@ -4656,18 +4882,20 @@ def genAnchors(s = nil, set = [], tag = :box)
n = 0
id = s.respond_to?(:nameString) ? "#{s.nameString}: " : ""
pts = poly(s)
- return invalid("#{id} polygon", mth, 1, DBG, n) if pts.empty?
- return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
+ return invalid("#{id} polygon", mth, 1, DBG, n) if pts.empty?
+ return mismatch("set", set, Array, mth, DBG, n) unless set.respond_to?(:to_a)
+ return mismatch("tag", tag, Symbol, mth, DBG, n) unless tag.respond_to?(:to_sym)
origin = OpenStudio::Point3d.new(0,0,0)
zenith = OpenStudio::Point3d.new(0,0,1)
ray = zenith - origin
set = set.to_a
+ tag = tag.to_sym
# Validate individual subsets. Purge surface-specific leader line anchors.
set.each_with_index do |st, i|
str1 = id + "subset ##{i+1}"
- str2 = str1 + " #{tag.to_s}"
+ str2 = str1 + " #{trim(tag)}"
return mismatch(str1, st, Hash, mth, DBG, n) unless st.respond_to?(:key?)
return hashkey( str1, st, tag, mth, DBG, n) unless st.key?(tag)
return empty("#{str2} vertices", mth, DBG, n) if st[tag].empty?
@@ -4810,7 +5038,7 @@ def genAnchors(s = nil, set = [], tag = :box)
# @param s [Set] a larger (parent) set of points
# @param [Array] set a collection of (smaller) sequenced vertices
# @option set [Hash] :ld a polygon-specific leader line anchors
- # @option [Symbol] tag sequence of set vertices to target
+ # @option [#to_sym] tag sequence of set vertices to target
#
# @return [OpenStudio::Point3dVector] extended vertices (see logs if empty)
def genExtendedVertices(s = nil, set = [], tag = :vtx)
@@ -4822,14 +5050,16 @@ def genExtendedVertices(s = nil, set = [], tag = :vtx)
a = OpenStudio::Point3dVector.new
v = []
return a if pts.empty?
- return mismatch("set", set, Array, mth, DBG, a) unless set.respond_to?(:to_a)
+ return mismatch("set", set, Array, mth, DBG, a) unless set.respond_to?(:to_a)
+ return mismatch("tag", tag, Symbol, mth, DBG, n) unless tag.respond_to?(:to_sym)
set = set.to_a
+ tag = tag.to_sym
# Validate individual sets.
set.each_with_index do |st, i|
str1 = id + "subset ##{i+1}"
- str2 = str1 + " #{tag.to_s}"
+ str2 = str1 + " #{trim(tag)}"
return mismatch(str1, st, Hash, mth, DBG, a) unless st.respond_to?(:key?)
next if st.key?(:void) && st[:void]
@@ -5121,8 +5351,8 @@ def genInserts(s = nil, set = [])
# surface type filters if 'type' argument == "all".
#
# @param spaces [Set] target spaces
- # @param boundary [#to_s] OpenStudio outside boundary condition
- # @param type [#to_s] OpenStudio surface (or subsurface) type
+ # @param boundary [#to_sym] OpenStudio outside boundary condition
+ # @param type [#to_sym] OpenStudio surface (or subsurface) type
# @param sides [Set] direction keys, e.g. :north (see OSut::SIDZ)
#
# @return [Array] surfaces (may be empty, no logs)
@@ -5131,7 +5361,7 @@ def facets(spaces = [], boundary = "all", type = "all", sides = [])
spaces = spaces.respond_to?(:to_a) ? spaces.to_a : []
return [] if spaces.empty?
- sides = sides.respond_to?(:to_sym) ? [sides] : sides
+ sides = sides.respond_to?(:to_sym) ? [trim(sides).to_sym] : sides
sides = sides.respond_to?(:to_a) ? sides.to_a : []
faces = []
@@ -5405,7 +5635,7 @@ def daylit?(space = nil, sidelit = true, toplit = true, baselit = true)
# @param s [OpenStudio::Model::Surface] a model surface
# @param [Array] subs requested attributes
# @option subs [#to_s] :id identifier e.g. "Window 007"
- # @option subs [#to_s] :type ("FixedWindow") OpenStudio subsurface type
+ # @option subs [#to_sym] :type ("FixedWindow") OpenStudio subsurface type
# @option subs [#to_i] :count (1) number of individual subs per array
# @option subs [#to_i] :multiplier (1) OpenStudio subsurface multiplier
# @option subs [#frameWidth] :frame (nil) OpenStudio frame & divider object
@@ -5534,12 +5764,13 @@ def addSubs(s = nil, subs = [], clear = false, bound = false, realign = false, b
return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl3)
# Required key:value pairs (either set by the user or defaulted).
- sub[:frame ] = nil unless sub.key?(:frame )
- sub[:assembly ] = nil unless sub.key?(:assembly )
- sub[:count ] = 1 unless sub.key?(:count )
+ sub[:frame ] = nil unless sub.key?(:frame)
+ sub[:assembly ] = nil unless sub.key?(:assembly)
+ sub[:count ] = 1 unless sub.key?(:count)
sub[:multiplier] = 1 unless sub.key?(:multiplier)
- sub[:id ] = "" unless sub.key?(:id )
- sub[:type ] = type unless sub.key?(:type )
+ sub[:id ] = "" unless sub.key?(:id)
+ sub[:type ] = type unless sub.key?(:type)
+ sub[:type ] = type unless sub[:type].respond_to?(:to_sym)
sub[:type ] = trim(sub[:type])
sub[:id ] = trim(sub[:id])
sub[:type ] = type if sub[:type].empty?
@@ -6368,7 +6599,7 @@ def toToplit(spaces = [], opts = {})
# @option opts [Bool] :sloped (true) whether to consider sloped roof surfaces
# @option opts [Bool] :plenum (true) whether to consider plenum wells
# @option opts [Bool] :attic (true) whether to consider attic wells
- # @option opts [Array<#to_s>] :patterns requested skylight allocation (3x)
+ # @option opts [Array<#to_sym>] :patterns requested skylight allocation (3x)
# @example (a) consider 2D array of individual skylights, e.g. n(1.22m x 1.22m)
# opts[:patterns] = ["array"]
# @example (b) consider 'a', then array of 1x(size) x n(size) skylight strips
@@ -6685,7 +6916,7 @@ def addSkyLights(spaces = [], opts = {})
if opts.key?(:patterns)
if opts[:patterns].is_a?(Array)
opts[:patterns].each_with_index do |pattern, i|
- pattern = trim(pattern).downcase
+ pattern = pattern.respond_to?(:to_sym) ? trim(pattern).downcase : ""
if pattern.empty?
invalid("pattern #{i+1}", mth, 0, ERR)
diff --git a/lib/tbd.rb b/lib/tbd.rb
index 06054de..760e676 100644
--- a/lib/tbd.rb
+++ b/lib/tbd.rb
@@ -58,16 +58,22 @@
end
module TBD
- extend OSut # OpenStudio utilities
-
- TOL = OSut::TOL.dup # default distance tolerance (m)
- TOL2 = OSut::TOL2.dup # default area tolerance (m2)
- DBG = OSut::DEBUG.dup # github.com/rd2/oslg
- INF = OSut::INFO.dup # github.com/rd2/oslg
- WRN = OSut::WARN.dup # github.com/rd2/oslg
- ERR = OSut::ERR.dup # github.com/rd2/oslg
- FTL = OSut::FATAL.dup # github.com/rd2/oslg
- NS = OSut::NS.dup # OpenStudio IdfObject nameString method
-
+ extend OSut # OpenStudio utilities (github.com/rd2/oslg)
+ DBG = OSut::DEBUG.dup
+ INF = OSut::INFO.dup
+ WRN = OSut::WARN.dup
+ ERR = OSut::ERR.dup
+ FTL = OSut::FATAL.dup
+ NS = OSut::NS.dup
+ TOL = OSut::TOL.dup
+ TOL2 = OSut::TOL2.dup
+ DMIN = OSut::DMIN.dup
+ DMAX = OSut::DMAX.dup
+ KMIN = OSut::KMIN.dup
+ KMAX = OSut::KMAX.dup
+ UMAX = OSut::UMAX.dup
+ UMIN = OSut::UMIN.dup
+ RMIN = OSut::RMIN.dup
+ RMAX = OSut::RMAX.dup
extend TBD
end
diff --git a/lib/tbd/geo.rb b/lib/tbd/geo.rb
index 7dd2a6f..34e8f07 100644
--- a/lib/tbd/geo.rb
+++ b/lib/tbd/geo.rb
@@ -310,23 +310,24 @@ def properties(surface = nil, argh = {})
end
unless surface.construction.empty?
- construction = surface.construction.get.to_LayeredConstruction
+ lc = surface.construction.get.to_LayeredConstruction
- unless construction.empty?
- construction = construction.get
- lyr = insulatingLayer(construction)
- lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
- lyr[:index] = nil unless lyr[:index] >= 0
- lyr[:index] = nil unless lyr[:index] < construction.layers.size
+ unless lc.empty?
+ lc = lc.get
+ lyr = insulatingLayer(lc)
- if lyr[:index]
- surf[:construction] = construction
+ if lyr[:index].is_a?(Integer) && lyr[:index].between?(0, lc.numLayers - 1)
+ surf[:construction] = lc
# index: ... of layer/material (to derate) within construction
# ltype: either :massless (RSi) or :standard (k + d)
# r : initial RSi value of the indexed layer to derate
surf[:index] = lyr[:index]
surf[:ltype] = lyr[:type ]
surf[:r ] = lyr[:r ]
+ else
+ surf[:index] = nil
+ surf[:ltype] = nil
+ surf[:r ] = 0.0
end
end
end
diff --git a/lib/tbd/psi.rb b/lib/tbd/psi.rb
index 8075770..d80874c 100644
--- a/lib/tbd/psi.rb
+++ b/lib/tbd/psi.rb
@@ -1355,103 +1355,103 @@ def inputs(s = {}, e = {}, argh = {})
##
# Thermally derates insulating material within construction.
#
- # @param id [#to_s] surface identifier
+ # @param id [#to_sym] surface identifier
# @param [Hash] s TBD surface parameters
- # @option s [#to_f] :heatloss heat loss from major thermal bridging, in W/K
- # @option s [#to_f] :net surface net area, in m2
+ # @option s [Numeric] :heatloss heat loss from major thermal bridging, in W/K
+ # @option s [Numeric] :net surface net area, in m2
# @option s [:massless, :standard] :ltype indexed layer type
- # @option s [#to_i] :index deratable construction layer index
- # @option s [#to_f] :r deratable layer Rsi-factor, in m2•K/W
+ # @option s [Integer] :index deratable construction layer index
+ # @option s [Numeric] :r deratable layer Rsi-factor, in m2•K/W
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
#
- # @return [OpenStudio::Model::Material] derated (cloned) material
+ # @return [OpenStudio::Model::OpaqueMaterial] derated (cloned) material
# @return [nil] if invalid input (see logs)
def derate(id = "", s = {}, lc = nil)
mth = "TBD::#{__callee__}"
m = nil
- id = trim(id)
kys = [:heatloss, :net, :ltype, :index, :r]
- ck1 = s.is_a?(Hash)
- ck2 = lc.is_a?(OpenStudio::Model::LayeredConstruction)
- return mismatch("id" , id, cl6, mth) if id.empty?
- return mismatch("#{id} surface" , s , cl1, mth) unless ck1
- return mismatch("#{id} construction", lc, cl2, mth) unless ck2
+ cl = OpenStudio::Model::LayeredConstruction
+ return mismatch("lc", lc, cl, mth) unless lc.respond_to?(NS)
+ return mismatch("id", id, String, mth) unless id.respond_to?(:to_sym)
+
+ id = trim(id)
+ nom = lc.nameString
+ return invalid("id", mth, 1) if id.empty?
+ return mismatch(nom, lc, cl, mth) unless lc.is_a?(cl)
+ return mismatch("#{nom} surface", s, Hash, mth) unless s.is_a?(Hash)
+
+ if nom.downcase.include?(" tbd")
+ log(WRN, "Won't derate '#{nom}': tagged as derated (#{mth})")
+ return m
+ end
kys.each do |k|
tag = "#{id} #{k}"
- return hashkey(tag, s, k, mth, ERR) unless s.key?(k)
+ return hashkey(tag, s, k, mth) unless s.key?(k)
case k
- when :heatloss
- return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_f)
- return zero(tag, mth, WRN) if s[k].to_f.abs < 0.001
- when :net, :r
- return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_f)
- return negative(tag, mth, 2, ERR) if s[k].to_f < 0
- return zero(tag, mth, WRN) if s[k].to_f.abs < 0.001
- when :index
- return mismatch(tag, s[k], Numeric, mth) unless s[k].respond_to?(:to_i)
- return negative(tag, mth, 2, ERR) if s[k].to_f < 0
- else # :ltype
+ when :ltype
next if [:massless, :standard].include?(s[k])
- return invalid(tag, mth, 2, ERR)
- end
- end
+ return invalid(tag, mth, 2)
+ when :index
+ return mismatch(tag, s[k], Integer, mth) unless s[k].is_a?(Integer)
+ return invalid(tag, mth, 2) unless s[k].between?(0, lc.numLayers - 1)
+ else
+ return mismatch(tag, s[k], Numeric, mth) unless s[k].is_a?(Numeric)
+ next if k == :heatloss
- if lc.nameString.downcase.include?(" tbd")
- log(WRN, "Won't derate '#{id}': tagged as derated (#{mth})")
- return m
+ return negative(tag, mth, 2) if s[k] < 0
+ return zero(tag, mth) if s[k].abs < 0.001
+ end
end
model = lc.model
- ltype = s[:ltype ]
- index = s[:index ].to_i
- net = s[:net ].to_f
- r = s[:r ].to_f
- u = s[:heatloss].to_f / net
+ ltype = s[:ltype]
+ index = s[:index]
+ net = s[:net]
+ r = s[:r]
+ u = s[:heatloss] / net
loss = 0
- de_u = 1 / r + u # derated U
- de_r = 1 / de_u # derated R
+ de_u = 1 / r + u # derated insulating material U
+ de_r = 1 / de_u # derated insulating material R
if ltype == :massless
- m = lc.getLayer(index).to_MasslessOpaqueMaterial
+ m = lc.getLayer(index).to_MasslessOpaqueMaterial
return invalid("#{id} massless layer?", mth, 0) if m.empty?
- m = m.get
- up = ""
- up = "uprated " if m.nameString.downcase.include?(" uprated")
- m = m.clone(model).to_MasslessOpaqueMaterial.get
- m.setName("#{id} #{up}m tbd")
- de_r = 0.001 unless de_r > 0.001
- loss = (de_u - 1 / de_r) * net unless de_r > 0.001
- m.setThermalResistance(de_r)
+
+ m = m.get
+ up = m.nameString.downcase.include?(" uprated") ? "uprated " : ""
+ m = m.clone(model).to_MasslessOpaqueMaterial.get
+ m.setName("#{id} #{up}m tbd")
+
+ de_r = RMIN unless de_r > RMIN
+ loss = (de_u - 1 / de_r) * net unless de_r > RMIN
+
+ unless m.setThermalResistance(de_r)
+ return invalid("Can't derate #{id}: RSi#{de_r.round(2)}", mth)
+ end
else
- m = lc.getLayer(index).to_StandardOpaqueMaterial
+ m = lc.getLayer(index).to_StandardOpaqueMaterial
return invalid("#{id} standard layer?", mth, 0) if m.empty?
- m = m.get
- up = ""
- up = "uprated " if m.nameString.downcase.include?(" uprated")
- m = m.clone(model).to_StandardOpaqueMaterial.get
- m.setName("#{id} #{up}m tbd")
- k = m.thermalConductivity
-
- if de_r > 0.001
- d = de_r * k
-
- unless d > 0.003
- d = 0.003
- k = d / de_r
- k = 3 unless k < 3
- loss = (de_u - k / d) * net unless k < 3
- end
- else # de_r < 0.001 m2•K/W
- d = 0.001 * k
- d = 0.003 unless d > 0.003
- k = d / 0.001 unless d > 0.003
- loss = (de_u - k / d) * net
+
+ m = m.get
+ up = m.nameString.downcase.include?(" uprated") ? "uprated " : ""
+ m = m.clone(model).to_StandardOpaqueMaterial.get
+ m.setName("#{id} #{up}m tbd")
+
+ d = m.thickness
+ k = (d / de_r).clamp(KMIN, KMAX)
+ d = (k * de_r).clamp(DMIN, DMAX)
+
+ loss = (de_u - k / d) * net unless d / k > RMIN
+
+ unless m.setThermalConductivity(k)
+ return invalid("Can't derate #{id}: K#{k.round(3)}", mth)
end
- m.setThickness(d)
- m.setThermalConductivity(k)
+ unless m.setThickness(d)
+ return invalid("Can't derate #{id}: #{(d*1000).to_i}mm", mth)
+ end
end
if m && loss > TOL
@@ -1941,14 +1941,38 @@ def process(model = nil, argh = {})
ids = windows.keys + doors.keys + skylights.keys
end
- unless ids.include?(i)
- log(ERR, "Orphaned subsurface #{i} (mth)")
+ adj = nil
+
+ unless ids.include?(i) # adjacent sub surface?
+ sb = model.getSubSurfaceByName(i)
+
+ if sb.empty?
+ log(DBG, "Orphaned subsurface #{i} (#{mth})?")
+ else
+ sb = sb.get
+ adj = sb.adjacentSubSurface
+
+ if adj.empty?
+ log(DBG, "Orphaned sub #{i} (#{mth})?")
+ end
+ end
+
next
end
- window = windows.key?(i) ? windows[i] : {}
- door = doors.key?(i) ? doors[i] : {}
- skylight = skylights.key?(i) ? skylights[i] : {}
+ if adj
+ window = windows.key?(i) ? windows[i] : {}
+ door = doors.key?(i) ? doors[i] : {}
+ skylight = skylights.key?(i) ? skylights[i] : {}
+ else
+ window = windows.key?(i) ? windows[i] : {}
+ door = doors.key?(i) ? doors[i] : {}
+ skylight = skylights.key?(i) ? skylights[i] : {}
+ end
+
+ # window = windows.key?(i) ? windows[i] : {}
+ # door = doors.key?(i) ? doors[i] : {}
+ # skylight = skylights.key?(i) ? skylights[i] : {}
sub = window unless window.empty?
sub = door unless door.empty?
@@ -2939,12 +2963,12 @@ def process(model = nil, argh = {})
up = argh[:uprate_walls] || argh[:uprate_roofs] || argh[:uprate_floors]
uprate(model, tbd[:surfaces], argh) if up
- # Derated (cloned) constructions are unique to each deratable surface.
- # Unique construction names are prefixed with the surface name,
- # and suffixed with " tbd", indicating that the construction is
- # henceforth thermally derated. The " tbd" expression is also key in
- # avoiding inadvertent derating - TBD will not derate constructions
- # (or rather layered materials) having " tbd" in their OpenStudio name.
+ # A derated (cloned) construction and (cloned) insulating layer are unique
+ # to each deratable surface. Unique construction and material names are
+ # prefixed with the surface name, and suffixed with " tbd", indicating that
+ # the construction is henceforth thermally derated. The " tbd" expression
+ # is also key in avoiding inadvertent sequential derating - TBD will not
+ # derate a construction/material pair having " tbd" in their OpenStudio name.
tbd[:surfaces].each do |id, surface|
next unless surface.key?(:construction)
next unless surface.key?(:index)
@@ -3138,9 +3162,9 @@ def exit(runner = nil, argh = {})
argh[:uprate_walls ] = false unless argh.key?(:uprate_walls )
argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs )
argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
- argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut )
- argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut )
- argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut )
+ argh[:wall_ut ] = UMAX unless argh.key?(:wall_ut )
+ argh[:roof_ut ] = UMAX unless argh.key?(:roof_ut )
+ argh[:floor_ut ] = UMAX unless argh.key?(:floor_ut )
argh[:wall_option ] = "" unless argh.key?(:wall_option )
argh[:roof_option ] = "" unless argh.key?(:roof_option )
argh[:floor_option ] = "" unless argh.key?(:floor_option )
diff --git a/lib/tbd/ua.rb b/lib/tbd/ua.rb
index 41c1d5b..bfbedf9 100644
--- a/lib/tbd/ua.rb
+++ b/lib/tbd/ua.rb
@@ -54,12 +54,12 @@ def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
lyr[:index] = nil unless lyr[:index].is_a?(Numeric)
lyr[:index] = nil unless lyr[:index] >= 0
lyr[:index] = nil unless lyr[:index] < lc.layers.size
- return invalid("#{id} layer index", mth, 3, ERR, res) unless lyr[:index]
+ return invalid("#{id} layer index", mth, 3, WRN, res) unless lyr[:index]
return zero("#{id}: heatloss" , mth, WRN, res) unless hloss > TOL
return zero("#{id}: films" , mth, WRN, res) unless film > TOL
- return zero("#{id}: Ut" , mth, WRN, res) unless ut > TOL
- return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < 5.678
- return zero("#{id}: net area (m2)", mth, ERR, res) unless area > TOL
+ return zero("#{id}: Ut" , mth, WRN, res) unless ut > UMIN
+ return invalid("#{id}: Ut" , mth, 6, WRN, res) unless ut < UMAX
+ return zero("#{id}: net area (m2)", mth, WRN, res) unless area > TOL
# First, calculate initial layer RSi to initially meet Ut target.
rt = 1 / ut # target construction Rt
@@ -71,56 +71,51 @@ def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
u_psi = hloss / area # from psi+khi
new_u -= u_psi # uprated layer USi to counter psi+khi
new_r = 1 / new_u # uprated layer RSi to counter psi+khi
- return zero("#{id}: new Rsi", mth, ERR, res) unless new_r > 0.001
+ return zero("#{id}: new Rsi", mth, WRN, res) unless new_r > RMIN
if lyr[:type] == :massless
- m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
- return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
-
- m = m.get.clone(model).to_MasslessOpaqueMaterial.get
- m.setName("#{id} uprated")
- new_r = 0.001 unless new_r > 0.001
- loss = (new_u - 1 / new_r) * area unless new_r > 0.001
- m.setThermalResistance(new_r)
- else # type == :standard
- m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
- return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
-
- m = m.get.clone(model).to_StandardOpaqueMaterial.get
- m.setName("#{id} uprated")
- k = m.thermalConductivity
-
- if new_r > 0.001
- d = new_r * k
-
- unless d > 0.003
- d = 0.003
- k = d / new_r
- k = 3.0 unless k < 3.0
- loss = (new_u - k / d) * area unless k < 3.0
- end
- else # new_r < 0.001 m2•K/W
- d = 0.001 * k
- d = 0.003 unless d > 0.003
- k = d / 0.001 unless d > 0.003
- loss = (new_u - k / d) * area
+ m = lc.getLayer(lyr[:index]).to_MasslessOpaqueMaterial
+ return invalid("#{id} massless layer?", mth, 0, DBG, res) if m.empty?
+
+ m = m.get.clone(model).to_MasslessOpaqueMaterial.get
+ m.setName("#{id} uprated")
+
+ new_r = RMIN unless new_r > RMIN
+ loss = (new_u - 1 / new_r) * area unless new_r > RMIN
+
+ unless m.setThermalResistance(new_r)
+ return invalid("Can't uprate #{id}: RSi#{new_r.round(2)}", mth, 0, DBG, res)
end
+ else
+ m = lc.getLayer(lyr[:index]).to_StandardOpaqueMaterial
+ return invalid("#{id} standard layer?", mth, 0, DBG, res) if m.empty?
- if m.setThickness(d)
- m.setThermalConductivity(k)
- else
- return invalid("Can't uprate #{id}: #{d} > 3m", mth, 0, ERR, res)
+ m = m.get.clone(model).to_StandardOpaqueMaterial.get
+ m.setName("#{id} uprated")
+
+ d = m.thickness
+ k = (d / new_r).clamp(KMIN, KMAX)
+ d = (k * new_r).clamp(DMIN, DMAX)
+
+ loss = (new_u - k / d) * area unless d / k > RMIN
+
+ unless m.setThermalConductivity(k)
+ return invalid("Can't uprate #{id}: K#{k.round(3)}", mth, 0, DBG, res)
+ end
+
+ unless m.setThickness(d)
+ return invalid("Can't uprate #{id}: #{(d*1000).to_i}mm", mth, 0, DBG, res)
end
end
- return invalid("Can't ID insulating layer", mth, 0, ERR, res) unless m
+ return invalid("Can't ID insulating layer", mth, 0, DBG, res) unless m
lc.setLayer(lyr[:index], m)
uo = 1 / rsi(lc, film)
if loss > TOL
h_loss = format "%.3f", loss
- return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, ERR, res)
+ return invalid("Can't assign #{h_loss} W/K to #{id}", mth, 0, DBG, res)
end
res[:uo] = uo
@@ -145,9 +140,9 @@ def uo(model = nil, lc = nil, id = "", hloss = 0.0, film = 0.0, ut = 0.0)
# @option argh [Bool] :uprate_walls (false) whether to uprate walls
# @option argh [Bool] :uprate_roofs (false) whether to uprate roofs
# @option argh [Bool] :uprate_floors (false) whether to uprate floors
- # @option argh [#to_f] :wall_ut (5.678) uprated wall Usi-factor target
- # @option argh [#to_f] :roof_ut (5.678) uprated roof Usi-factor target
- # @option argh [#to_f] :floor_ut (5.678) uprated floor Usi-factor target
+ # @option argh [#to_f] :wall_ut (UMAX) uprated wall Usi-factor target
+ # @option argh [#to_f] :roof_ut (UMAX) uprated roof Usi-factor target
+ # @option argh [#to_f] :floor_ut (UMAX) uprated floor Usi-factor target
# @option argh [#to_s] :wall_option ("") construction to uprate (or "all")
# @option argh [#to_s] :roof_option ("") construction to uprate (or "all")
# @option argh [#to_s] :floor_option ("") construction to uprate (or "all")
@@ -172,9 +167,9 @@ def uprate(model = nil, s = {}, argh = {})
argh[:uprate_walls ] = false unless argh.key?(:uprate_walls)
argh[:uprate_roofs ] = false unless argh.key?(:uprate_roofs)
argh[:uprate_floors] = false unless argh.key?(:uprate_floors)
- argh[:wall_ut ] = 5.678 unless argh.key?(:wall_ut)
- argh[:roof_ut ] = 5.678 unless argh.key?(:roof_ut)
- argh[:floor_ut ] = 5.678 unless argh.key?(:floor_ut)
+ argh[:wall_ut ] = UMAX unless argh.key?(:wall_ut)
+ argh[:roof_ut ] = UMAX unless argh.key?(:roof_ut)
+ argh[:floor_ut ] = UMAX unless argh.key?(:floor_ut)
argh[:wall_option ] = "" unless argh.key?(:wall_option)
argh[:roof_option ] = "" unless argh.key?(:roof_option)
argh[:floor_option ] = "" unless argh.key?(:floor_option)
@@ -197,7 +192,7 @@ def uprate(model = nil, s = {}, argh = {})
groups.each do |type, g|
next unless g[:up]
next unless g[:ut].is_a?(Numeric)
- next unless g[:ut] < 5.678
+ next unless g[:ut] < UMAX
next if g[:ut] < 0
typ = type
@@ -211,7 +206,7 @@ def uprate(model = nil, s = {}, argh = {})
all = tout.include?(op)
if g[:op].empty?
- log(ERR, "Construction (#{type}) to uprate? (#{mth})")
+ log(WRN, "Construction (#{type}) to uprate? (#{mth})")
elsif all
s.each do |nom, surface|
next unless surface.key?(:deratable )
@@ -247,11 +242,11 @@ def uprate(model = nil, s = {}, argh = {})
else
id = g[:op]
lc = model.getConstructionByName(id)
- log(ERR, "Construction '#{id}'? (#{mth})") if lc.empty?
+ log(WRN, "Construction '#{id}'? (#{mth})") if lc.empty?
next if lc.empty?
lc = lc.get.to_LayeredConstruction
- log(ERR, "'#{id}' layered construction? (#{mth})") if lc.empty?
+ log(WRN, "'#{id}' layered construction? (#{mth})") if lc.empty?
next if lc.empty?
lc = lc.get
@@ -282,7 +277,7 @@ def uprate(model = nil, s = {}, argh = {})
end
if coll.empty?
- log(ERR, "No #{type} construction to uprate - skipping (#{mth})")
+ log(WRN, "No #{type} construction to uprate - skipping (#{mth})")
next
elsif lc
# Valid layered construction - good to uprate!
@@ -291,7 +286,7 @@ def uprate(model = nil, s = {}, argh = {})
lyr[:index] = nil unless lyr[:index] >= 0
lyr[:index] = nil unless lyr[:index] < lc.layers.size
- log(ERR, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
+ log(WRN, "Insulation index for '#{id}'? (#{mth})") unless lyr[:index]
next unless lyr[:index]
# Ensure lc is exclusively linked to deratable surfaces of right type.
@@ -302,10 +297,10 @@ def uprate(model = nil, s = {}, argh = {})
next unless surface.key?(:construction)
next unless surface[:construction].is_a?(cl3)
next unless surface[:construction] == lc
+ next unless surface[:deratable]
ok = true
- ok = false unless surface[:type ] == typ
- ok = false unless surface[:deratable]
+ ok = false unless surface[:type] == typ
ok = false unless coll.key?(id)
ok = false unless coll[id][:s].key?(nom)
@@ -346,13 +341,18 @@ def uprate(model = nil, s = {}, argh = {})
if sss.isConstructionDefaulted
set = defaultConstructionSet(sss) # building? story?
- constructions = set.defaultExteriorSurfaceConstructions
- unless constructions.empty?
- constructions = constructions.get
- constructions.setWallConstruction(lc) if typ == :wall
- constructions.setFloorConstruction(lc) if typ == :floor
- constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
+ if set.nil?
+ sss.setConstruction(lc)
+ else
+ constructions = set.defaultExteriorSurfaceConstructions
+
+ unless constructions.empty?
+ constructions = constructions.get
+ constructions.setWallConstruction(lc) if typ == :wall
+ constructions.setFloorConstruction(lc) if typ == :floor
+ constructions.setRoofCeilingConstruction(lc) if typ == :ceiling
+ end
end
else
sss.setConstruction(lc)
@@ -385,7 +385,7 @@ def uprate(model = nil, s = {}, argh = {})
res = uo(model, lc, id, hloss, film, g[:ut])
unless res[:uo] && res[:m]
- log(ERR, "Unable to uprate '#{id}' (#{mth})")
+ log(WRN, "Unable to uprate '#{id}' (#{mth})")
next
end
@@ -407,7 +407,7 @@ def uprate(model = nil, s = {}, argh = {})
argh[:roof_uo ] = res[:uo] if typ == :ceiling
argh[:floor_uo] = res[:uo] if typ == :floor
else
- log(ERR, "Nilled construction to uprate - (#{mth})")
+ log(WRN, "Nilled construction to uprate - (#{mth})")
return false
end
end
@@ -589,7 +589,7 @@ def ua_summary(date = Time.now, argh = {})
empty = shorts[:has].empty? && shorts[:val].empty?
has = shorts[:has] unless empty
val = shorts[:val] unless empty
- log(ERR, "Invalid UA' reference set (#{mth})") if empty
+ log(WRN, "Invalid UA' reference set (#{mth})") if empty
unless empty
ua[:model] += " : Design vs '#{ref}'"
@@ -1000,7 +1000,7 @@ def ua_md(ua = {}, lang = :en)
model = "* modèle : #{ua[:file]}" if ua.key?(:file) && lang == :fr
model += " (v#{ua[:version]})" if ua.key?(:version)
report << model unless model.empty?
- report << "* TBD : v3.4.5"
+ report << "* TBD : v3.5.0"
report << "* date : #{ua[:date]}"
if lang == :en
diff --git a/lib/tbd/version.rb b/lib/tbd/version.rb
index 9f174ca..3634cf3 100644
--- a/lib/tbd/version.rb
+++ b/lib/tbd/version.rb
@@ -21,5 +21,5 @@
# SOFTWARE.
module TBD
- VERSION = "3.4.5".freeze
+ VERSION = "3.5.0".freeze
end
diff --git a/spec/tbd_tests_spec.rb b/spec/tbd_tests_spec.rb
index 9e648b8..b6fae36 100644
--- a/spec/tbd_tests_spec.rb
+++ b/spec/tbd_tests_spec.rb
@@ -9,6 +9,14 @@
WRN = TBD::WRN.dup
ERR = TBD::ERR.dup
FTL = TBD::FTL.dup
+ DMIN = TBD::DMIN.dup
+ DMAX = TBD::DMAX.dup
+ KMIN = TBD::KMIN.dup
+ KMAX = TBD::KMAX.dup
+ UMAX = TBD::UMAX.dup
+ UMIN = TBD::UMIN.dup
+ RMIN = TBD::RMIN.dup
+ RMAX = TBD::RMAX.dup
it "can process JSON surface KHI entries" do
translator = OpenStudio::OSVersion::VersionTranslator.new
@@ -1565,69 +1573,6 @@
model.save(file, true)
end
- it "can purge KIVA objects" do
- translator = OpenStudio::OSVersion::VersionTranslator.new
- TBD.clean!
-
- file = File.join(__dir__, "files/osms/out/seb_KIVA.osm")
- path = OpenStudio::Path.new(file)
- model = translator.loadModel(path)
- expect(model).to_not be_empty
- model = model.get
-
- expect(model.foundationKivaSettings).to be_empty
- expect(model.getSurfacePropertyExposedFoundationPerimeters.size).to eq(1)
- expect(model.getFoundationKivas.size).to eq(4)
-
- adjacents = 0
- foundation = nil
-
- model.getSurfaces.each do |surface|
- next unless surface.isGroundSurface
- next if surface.adjacentFoundation.empty?
-
- adjacents += 1
- foundation = surface.adjacentFoundation.get
- expect(surface.surfacePropertyExposedFoundationPerimeter).to_not be_empty
- expect(surface.outsideBoundaryCondition.downcase).to eq("foundation")
- end
-
- expect(adjacents).to eq(1)
- expect(foundation).to be_a(OpenStudio::Model::FoundationKiva)
-
- # Add 2x custom blocks for testing.
- xps = model.getMaterialByName("XPS_38mm")
- expect(xps).to_not be_empty
- xps = xps.get
- expect(foundation.addCustomBlock(xps, 0.1, 0.1, -0.5)).to be true
- expect(foundation.addCustomBlock(xps, 0.2, 0.2, -1.5)).to be true
-
- blocks = foundation.customBlocks
- expect(blocks).to_not be_empty
-
- blocks.each { |block| expect(block.material).to eq(xps) }
-
- # Purge.
- expect(TBD.resetKIVA(model, "Ground")).to be true
- expect(model.foundationKivaSettings).to be_empty
- expect(model.getSurfacePropertyExposedFoundationPerimeters).to be_empty
- expect(model.getFoundationKivas).to be_empty
- expect(TBD.info?).to be true
- expect(TBD.logs.size).to eq(1)
- expect(TBD.logs.first[:message]).to include("Purged KIVA objects from ")
-
- model.getSurfaces.each do |surface|
- next unless surface.isGroundSurface
-
- expect(surface.adjacentFoundation).to be_empty
- expect(surface.surfacePropertyExposedFoundationPerimeter).to be_empty
- expect(surface.outsideBoundaryCondition).to eq("Ground")
- end
-
- file = File.join(__dir__, "files/osms/out/seb_noKIVA.osm")
- model.save(file, true)
- end
-
it "can test 5ZoneNoHVAC (failed) uprating" do
translator = OpenStudio::OSVersion::VersionTranslator.new
TBD.clean!
@@ -1703,8 +1648,8 @@
# linear thermal bridges is very high given the limited exposed (gross)
# area. If area-weighted, derating the insulation layer of the referenced
# wall construction above would entail factoring in this extra thermal
- # conductance of ~0.309 W/m2•K (84.6/273.6), which would reduce the
- # insulation thickness quite significantly.
+ # conductance of ~0.309 W/m2•K (84.6/273.6), which would increase the
+ # insulation conductivity quite significantly.
#
# Ut = Uo + ( ∑psi • L )/A
#
@@ -1757,7 +1702,9 @@
#
# The method exits with an ERROR in 2x cases:
# - calculated Uo is negative, i.e. ( ∑psi • L )/A > 0.277
- # - calculated layer r violates E+ material constraints (e.g. too thin)
+ # - calculated layer r violates E+ material constraints, e.g.
+ # - too conductive
+ # - too thin
# -- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- #
# Retrying the previous example, yet requesting uprating calculations:
@@ -1782,7 +1729,7 @@
expect(json).to have_key(:surfaces)
io = json[:io ]
surfaces = json[:surfaces]
- expect(TBD.error?).to be true
+ expect(TBD.warn?).to be true
expect(TBD.logs.size).to eq(2)
expect(TBD.logs.first[:message]).to include("Zero")
expect(TBD.logs.first[:message]).to include(": new Rsi")
@@ -1846,10 +1793,10 @@
insul = insul.get
expect(insul.nameString).to include(" uprated m tbd")
- expect(insul.thermalConductivity).to be_within(0.0001).of(0.0432)
- th1 = (insul.thickness - 0.191).abs < 0.001 # derated layer Rsi 4.42
- th2 = (insul.thickness - 0.186).abs < 0.001 # derated layer Rsi 4.31
- expect(th1 || th2).to be true # depending if 'short' or 'long' walls
+ k1 = (insul.thermalConductivity - 0.0261).round(4) == 0
+ k2 = (insul.thermalConductivity - 0.0253).round(4) == 0
+ expect(k1 || k2).to be true
+ expect(insul.thickness).to be_within(0.0001).of(0.1120)
end
walls.each do |wall|
@@ -1924,21 +1871,45 @@
# layer thickness limit, harmonizing with EnergyPlus:
#
# https://github.com/NREL/OpenStudio/pull/4622
- if OpenStudio.openStudioVersion.split(".").join.to_i < 350
- expect(TBD.error?).to be true
- expect(TBD.logs).to_not be_empty
- expect(TBD.logs.size).to eq(2)
+ #
+ # This didn't mean EnergyPlus wouldn't halt a simulation due to invalid CTF
+ # calculations - happens with very thick materials. Recent 2025 TBD changes
+ # have removed this check. Users of pre-v3.5.X OpenStudio should expect
+ # OS-generated simulation failures when uprating (extremes cases). Achtung!
+ expect(TBD.status).to be_zero
+ expect(argh).to have_key(:wall_uo)
+ expect(argh[:wall_uo]).to be_within(0.0001).of(UMIN) # RSi 100 (R568)
- expect(TBD.logs.first[:message]).to include("Invalid")
- expect(TBD.logs.first[:message]).to include("Can't uprate ")
- expect(TBD.logs.last[:message ]).to include("Unable to uprate")
+ nb = 0
- expect(argh).to_not have_key(:wall_uo)
- else
- expect(TBD.status).to be_zero
- expect(argh).to have_key(:wall_uo)
- expect(argh[:wall_uo]).to be_within(0.0001).of(0.0089) # RSi 112 (R638)
+ model.getSurfaces.each do |s|
+ next unless s.surfaceType.downcase == "wall"
+
+ c = s.construction
+ expect(c).to_not be_empty
+ c = c.get.to_LayeredConstruction
+ next if c.empty?
+
+ c = c.get
+ next unless c.nameString.include?("c tbd")
+
+ lyr = TBD.insulatingLayer(c)
+ expect(lyr).to be_a(Hash)
+ expect(lyr).to have_key(:type)
+ expect(lyr).to have_key(:index)
+ expect(lyr).to have_key(:r)
+ expect(lyr[:type]).to eq(:standard)
+ expect(lyr[:index]).to be_between(0, c.numLayers)
+ insul = c.getLayer(lyr[:index])
+ insul = insul.to_StandardOpaqueMaterial
+ expect(insul).to_not be_empty
+ insul = insul.get
+ expect(insul.thickness).to be_within(TOL).of(1.00)
+
+ nb += 1
end
+
+ expect(nb).to eq(4)
end
it "can test Hash inputs" do
@@ -2035,7 +2006,6 @@
it "can check for attics vs plenums" do
translator = OpenStudio::OSVersion::VersionTranslator.new
TBD.clean!
-
# Outdoor-facing surfaces of UNCONDITIONED spaces are never derated by TBD.
# Yet determining whether an OpenStudio space should be considered
# UNCONDITIONED (e.g. an attic), rather than INDIRECTLYCONDITIONED
@@ -2168,6 +2138,95 @@
expect(attic.additionalProperties.resetFeature(key)).to be true
+ # Adding a sub surface between UNCONDITIONED Attic & CONDITIONED Core.
+ file = File.join(__dir__, "files/osms/in/smalloffice.osm")
+ path = OpenStudio::Path.new(file)
+ model = translator.loadModel(path)
+ expect(model).to_not be_empty
+ model = model.get
+
+ floor = model.getSurfaceByName("Attic_floor_core")
+ expect(floor).to_not be_empty
+ floor = floor.get
+
+ ceiling = floor.adjacentSurface
+ expect(ceiling).to_not be_empty
+ ceiling = ceiling.get
+
+ sub = {}
+ sub[:id ] = "attic trap door"
+ sub[:type ] = "Door"
+ sub[:assembly] = TBD.genConstruction(model, {type: :door})
+ sub[:width ] = 1.0
+ sub[:height ] = 1.0
+ expect(TBD.addSubs(floor, sub, false, true, true)).to be true
+ expect(TBD.addSubs(ceiling, sub, false, true, false)).to be true
+ expect(floor.subSurfaces.size).to eq(1)
+ expect(ceiling.subSurfaces.size).to eq(1)
+ trap = floor.subSurfaces.first
+ door = ceiling.subSurfaces.first
+ expect(trap.setAdjacentSubSurface(door)).to be true
+ expect(door.setAdjacentSubSurface(trap)).to be true
+ expect(trap.adjacentSubSurface).to_not be_empty
+ expect(door.adjacentSubSurface).to_not be_empty
+ expect(trap.adjacentSubSurface.get).to eq(door)
+ expect(door.adjacentSubSurface.get).to eq(trap)
+
+ argh = { option: "code (Quebec)" }
+ json = TBD.process(model, argh)
+ puts TBD.logs
+ expect(TBD.status).to be_zero
+ expect(json).to be_a(Hash)
+ expect(json).to have_key(:io)
+ expect(json).to have_key(:surfaces)
+ io = json[:io ]
+ surfaces = json[:surfaces]
+ expect(surfaces).to be_a(Hash)
+ expect(surfaces.size).to eq(43)
+ expect(io).to have_key(:edges)
+ expect(io[:edges].size).to eq(109)
+
+ file = File.join(__dir__, "files/osms/out/trapdoor.osm")
+ model.save(file, true)
+
+ # Adding skylights/wells.
+ file = File.join(__dir__, "files/osms/in/smalloffice.osm")
+ path = OpenStudio::Path.new(file)
+ model = translator.loadModel(path)
+ expect(model).to_not be_empty
+ model = model.get
+
+ srr = 0.05
+ gra = TBD.grossRoofArea(model.getSpaces)
+ tm2 = srr * gra
+ rm2 = TBD.addSkyLights(model.getSpaces, {area: tm2})
+ puts TBD.logs unless TBD.logs.empty?
+ expect(TBD.status).to be_zero
+ expect(rm2.round(2)).to eq(gra.round(2))
+
+ argh = {}
+ argh[:option ] = "efficient (BETBG)"
+ argh[:uprate_walls] = true
+ argh[:uprate_roofs] = true
+ argh[:wall_option ] = "ALL wall constructions"
+ argh[:roof_option ] = "ALL roof constructions"
+ argh[:wall_ut ] = 0.215 # NECB 2020 CZ7A (RSi 4.65 / R26)
+ argh[:roof_ut ] = 0.121 # NECB 2020 CZ7A (RSi 8.26 / R47)
+ json = TBD.process(model, argh)
+ expect(TBD.status).to be_zero
+ expect(json).to be_a(Hash)
+ expect(json).to have_key(:io)
+ expect(json).to have_key(:surfaces)
+ io = json[:io ]
+ surfaces = json[:surfaces]
+ expect(surfaces).to be_a(Hash)
+ expect(surfaces.size).to eq(79)
+ expect(io).to have_key(:edges)
+ expect(io[:edges].size).to eq(173)
+
+ file = File.join(__dir__, "files/osms/out/office_attic_sky.osm")
+ model.save(file, true)
+
# -- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- -- #
# 5Zone_2 test case (as INDIRECTLYCONDITIONED plenum).
plenum_walls = []
@@ -3101,6 +3160,9 @@
puts TBD.logs unless TBD.logs.empty?
expect(TBD.status).to be_zero
+
+ file = File.join(__dir__, "files/osms/out/seb2_sky2.osm")
+ model.save(file, true)
end
it "can factor in negative PSI-factors (JSON input)" do
diff --git a/tbd.gemspec b/tbd.gemspec
index 87a4ad5..a03bdfe 100644
--- a/tbd.gemspec
+++ b/tbd.gemspec
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
s.metadata = {}
s.add_dependency "topolys", "~> 0"
- s.add_dependency "osut", "~> 0.7.0"
+ s.add_dependency "osut", "~> 0.8.0"
s.add_dependency "json-schema", "~> 4"
s.add_development_dependency "bundler", "~> 2.1"