lib/tbd/psi.rb in tbd-3.2.3 vs lib/tbd/psi.rb in tbd-3.3.0
- old
+ new
@@ -19,13 +19,13 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
module TBD
- # Sources for thermal bridge types and default KHI & PSI values/sets:
+ # Sources for thermal bridge types and default KHI- & PSI-factor sets:
#
- # a) BETBG = Building Envelope Thermal Bridging Guide v1.4 (or higher):
+ # a) BETBG = Building Envelope Thermal Bridging Guide v1.4 (or newer):
#
# www.bchydro.com/content/dam/BCHydro/customer-portal/documents/power-smart/
# business/programs/BETB-Building-Envelope-Thermal-Bridging-Guide-v1-4.pdf
#
# b) ISO 14683 (Appendix C): www.iso.org/standard/65706.html
@@ -40,537 +40,1065 @@
##
# Library of point thermal bridges (e.g. columns). Each key:value entry
# requires a unique identifier e.g. "poor (BETBG)" and a KHI-value in W/K.
class KHI
+ extend OSut
+
# @return [Hash] KHI library
attr_reader :point
##
- # Construct a new KHI library (with defaults).
+ # Constructs a new KHI library (with defaults).
def initialize
@point = {}
- # The following are defaults. Users may edit these defaults,
- # append new key:value pairs, or even read-in other pairs on file.
- # Units are in W/K.
- @point["poor (BETBG)" ] = 0.900 # detail 5.7.2 BETBG
- @point["regular (BETBG)" ] = 0.500 # detail 5.7.4 BETBG
- @point["efficient (BETBG)" ] = 0.150 # detail 5.7.3 BETBG
- @point["code (Quebec)" ] = 0.500 # art. 3.3.1.3. NECB-QC
- @point["uncompliant (Quebec)" ] = 1.000 # Guide
- @point["(non thermal bridging)"] = 0.000
+ # The following are built-in KHI-factors. Users may append new key:value
+ # pairs, preferably through a TBD JSON input file. Units are in W/K.
+ @point["poor (BETBG)" ] = 0.900 # detail 5.7.2 BETBG
+ @point["regular (BETBG)" ] = 0.500 # detail 5.7.4 BETBG
+ @point["efficient (BETBG)" ] = 0.150 # detail 5.7.3 BETBG
+ @point["code (Quebec)" ] = 0.500 # art. 3.3.1.3. NECB-QC
+ @point["uncompliant (Quebec)" ] = 1.000 # NECB-QC Guide
+ @point["90.1.22|steel.m|default" ] = 0.480 # steel/metal, compliant
+ @point["90.1.22|steel.m|unmitigated"] = 0.920 # steel/metal, non-compliant
+ @point["90.1.22|mass.ex|default" ] = 0.330 # ext/integral, compliant
+ @point["90.1.22|mass.ex|unmitigated"] = 0.460 # ext/integral, non-compliant
+ @point["90.1.22|mass.in|default" ] = 0.330 # interior mass, compliant
+ @point["90.1.22|mass.in|unmitigated"] = 0.460 # interior, non-compliant
+ @point["90.1.22|wood.fr|default" ] = 0.040 # compliant
+ @point["90.1.22|wood.fr|unmitigated"] = 0.330 # non-compliant
+ @point["(non thermal bridging)" ] = 0.000 # defaults to 0
end
##
- # Append a new KHI entry, based on a TBD JSON-formatted KHI object (requires
- # a valid, unique :id key and valid :point value).
+ # Appends a new KHI entry.
#
- # @param k [Hash] a new KHI entry
+ # @param [Hash] k a new KHI entry
+ # @option k [#to_s] :id name
+ # @option k [#to_f] :point conductance, in W/K
#
- # @return [Bool] true if successfully appended
- # @return [Bool] false if invalid input
+ # @return [Bool] whether KHI entry is successfully appended
+ # @return [false] if invalid input (see logs)
def append(k = {})
mth = "TBD::#{__callee__}"
a = false
+ ck1 = k.respond_to?(:key?)
+ return mismatch("KHI" , k, Hash , mth, DBG, a) unless ck1
+ return hashkey("KHI id" , k, :id , mth, DBG, a) unless k.key?(:id)
+ return hashkey("KHI point", k, :point, mth, DBG, a) unless k.key?(:point)
- return TBD.mismatch("KHI", k, Hash, mth, DBG, a) unless k.is_a?(Hash)
- return TBD.hashkey("KHI id", k, :id, mth, DBG, a) unless k.key?(:id)
- return TBD.hashkey("KHI pt", k, :point, mth, DBG, a) unless k.key?(:point)
+ id = trim(k[:id])
+ ck1 = id.empty?
+ ck2 = k[:point].respond_to?(:to_f)
+ return mismatch("KHI id" , k[:id ], String, mth, ERR, a) if ck1
+ return mismatch("KHI point", k[:point], Float , mth, ERR, a) unless ck2
- if @point.key?(k[:id])
- TBD.log(ERR, "Skipping '#{k[:id]}': existing KHI entry (#{mth})")
+ if @point.key?(id)
+ log(ERR, "Skipping '#{id}': existing KHI entry (#{mth})")
return false
end
- @point[k[:id]] = k[:point]
+ @point[id] = k[:point].to_f
true
end
end
##
# Library of linear thermal bridges (e.g. corners, balconies). Each key:value
# entry requires a unique identifier e.g. "poor (BETBG)" and a (partial or
- # complete) set of PSI-values in W/K per linear meter.
+ # complete) set of PSI-factors in W/K per linear meter.
class PSI
+ extend OSut
+
# @return [Hash] PSI set
attr_reader :set
# @return [Hash] shorthand listing of PSI types in a set
attr_reader :has
- # @return [Hash] shorthand listing of PSI values in a set
+ # @return [Hash] shorthand listing of PSI-factors in a set
attr_reader :val
##
- # Construct a new PSI library (with defaults)
+ # Constructs a new PSI library (with defaults)
def initialize
@set = {}
@has = {}
@val = {}
- # The following are default PSI values (* published, ** calculated). Users
- # may edit these sets, add new sets here, or read-in custom sets from a
- # TBD JSON input file. PSI units are in W/K per linear meter. The spandrel
- # sets are added as practical suggestions in early design stages.
-
+ # The following are built-in PSI-factor sets, more often predefined sets
+ # published in guides or energy codes. Users may append new sets,
+ # preferably through a TBD JSON input file. Units are in W/K per meter.
+ #
+ # The provided "spandrel" sets are suitable for early design.
+ #
# Convex vs concave PSI adjustments may be warranted if there is a
# mismatch between dimensioning conventions (interior vs exterior) used
- # for the OpenStudio model (OSM) vs published PSI data. For instance, the
- # BETBG data reflects an interior dimensioning convention, while ISO
- # 14683 reports PSI values for both conventions. The following may be
- # used (with caution) to adjust BETBG PSI values for convex corners when
- # using outside dimensions for an OSM.
+ # for the OpenStudio model vs published PSI data. For instance, the BETBG
+ # data reflects an interior dimensioning convention, while ISO 14683
+ # reports PSI-factors for both conventions. The following may be used
+ # (with caution) to adjust BETBG PSI-factors for convex corners when
+ # using outside dimensions for an OpenStudio model.
#
# PSIe = PSIi + U * 2(Li-Le), where:
- # PSIe = adjusted PSI (W/K per m)
- # PSIi = initial published PSI (W/K per m)
- # U = average clear field U-factor of adjacent walls (W/m2.K)
- # Li = from interior corner to edge of "zone of influence" (m)
- # Le = from exterior corner to edge of "zone of influence" (m)
+ # PSIe = adjusted PSI W/K per m
+ # PSIi = initial published PSI, in W/K per m
+ # U = average clear field U-factor of adjacent walls, in W/m2•K
+ # Li = 'interior corner to edge' length of "zone of influence", in m
+ # Le = 'exterior corner to edge' length of "zone of influence", in m
#
# Li-Le = wall thickness e.g., -0.25m (negative here as Li < Le)
+
+ # Based on INTERIOR dimensioning (p.15 BETBG).
@set["poor (BETBG)"] =
{
- rimjoist: 1.000, # *
- parapet: 0.800, # *
- fenestration: 0.500, # *
- corner: 0.850, # *
- balcony: 1.000, # *
- party: 0.850, # *
- grade: 0.850, # *
- joint: 0.300, # *
- transition: 0.000
- }.freeze # based on INTERIOR dimensions (p.15 BETBG)
- self.gen("poor (BETBG)")
+ rimjoist: 1.000000, # re: BETBG
+ parapet: 0.800000, # re: BETBG
+ roof: 0.800000, # same as parapet
+ fenestration: 0.500000, # re: BETBG
+ door: 0.500000, # inferred, same as (vertical) fenestration
+ skylight: 0.500000, # inferred, same as (vertical) fenestration
+ spandrel: 0.155000, # Detail 5.4.4
+ corner: 0.850000, # re: BETBG
+ balcony: 1.000000, # re: BETBG
+ balconysill: 1.000000, # same as balcony
+ balconydoorsill: 1.000000, # same as balconysill
+ party: 0.850000, # re: BETBG
+ grade: 0.850000, # re: BETBG
+ joint: 0.300000, # re: BETBG
+ transition: 0.000000 # defaults to 0
+ }.freeze
+ # Based on INTERIOR dimensioning (p.15 BETBG).
@set["regular (BETBG)"] =
{
- rimjoist: 0.500, # *
- parapet: 0.450, # *
- fenestration: 0.350, # *
- corner: 0.450, # *
- balcony: 0.500, # *
- party: 0.450, # *
- grade: 0.450, # *
- joint: 0.200, # *
- transition: 0.000
- }.freeze # based on INTERIOR dimensions (p.15 BETBG)
- self.gen("regular (BETBG)")
+ rimjoist: 0.500000, # re: BETBG
+ parapet: 0.450000, # re: BETBG
+ roof: 0.450000, # same as parapet
+ fenestration: 0.350000, # re: BETBG
+ door: 0.350000, # inferred, same as (vertical) fenestration
+ skylight: 0.350000, # inferred, same as (vertical) fenestration
+ spandrel: 0.155000, # Detail 5.4.4
+ corner: 0.450000, # re: BETBG
+ balcony: 0.500000, # re: BETBG
+ balconysill: 0.500000, # same as balcony
+ balconydoorsill: 0.500000, # same as balconysill
+ party: 0.450000, # re: BETBG
+ grade: 0.450000, # re: BETBG
+ joint: 0.200000, # re: BETBG
+ transition: 0.000000 # defaults to 0
+ }.freeze
+ # Based on INTERIOR dimensioning (p.15 BETBG).
@set["efficient (BETBG)"] =
{
- rimjoist: 0.200, # *
- parapet: 0.200, # *
- fenestration: 0.200, # *
- corner: 0.200, # *
- balcony: 0.200, # *
- party: 0.200, # *
- grade: 0.200, # *
- joint: 0.100, # *
- transition: 0.000
- }.freeze # based on INTERIOR dimensions (p.15 BETBG)
- self.gen("efficient (BETBG)")
+ rimjoist: 0.200000, # re: BETBG
+ parapet: 0.200000, # re: BETBG
+ roof: 0.200000, # same as parapet
+ fenestration: 0.199999, # re: BETBG
+ door: 0.199999, # inferred, same as (vertical) fenestration
+ skylight: 0.199999, # inferred, same as (vertical) fenestration
+ spandrel: 0.155000, # Detail 5.4.4
+ corner: 0.200000, # re: BETBG
+ balcony: 0.200000, # re: BETBG
+ balconysill: 0.200000, # same as balcony
+ balconydoorsill: 0.200000, # same as balconysill
+ party: 0.200000, # re: BETBG
+ grade: 0.200000, # re: BETBG
+ joint: 0.100000, # re: BETBG
+ transition: 0.000000 # defaults to 0
+ }.freeze
+ # "Conventional", closer to window wall spandrels.
@set["spandrel (BETBG)"] =
{
- rimjoist: 0.615, # * Detail 1.2.1
- parapet: 1.000, # * Detail 1.3.2
- fenestration: 0.000, # * ... generally part of clear-field RSi
- corner: 0.425, # * Detail 1.4.1
- balcony: 1.110, # * Detail 8.1.9/9.1.6
- party: 0.990, # ** ... similar to parapet/balcony
- grade: 0.880, # * Detail 2.5.1
- joint: 0.500, # * Detail 3.3.2
- transition: 0.000
- }.freeze # "conventional", closer to window wall spandrels
- self.gen("spandrel (BETBG)")
+ rimjoist: 0.615000, # Detail 1.2.1
+ parapet: 1.000000, # Detail 1.3.2
+ roof: 1.000000, # same as parapet
+ fenestration: 0.000000, # inferred, generally part of clear-field RSi
+ door: 0.000000, # inferred, generally part of clear-field RSi
+ skylight: 0.350000, # same as "regular (BETBG)"
+ spandrel: 0.155000, # Detail 5.4.4
+ corner: 0.425000, # Detail 1.4.1
+ balcony: 1.110000, # Detail 8.1.9/9.1.6
+ balconysill: 1.110000, # same as balcony
+ balconydoorsill: 1.110000, # same as balconysill
+ party: 0.990000, # inferred, similar to parapet/balcony
+ grade: 0.880000, # Detail 2.5.1
+ joint: 0.500000, # Detail 3.3.2
+ transition: 0.000000 # defaults to 0
+ }.freeze
+ # "GoodHigh performance" curtainwall spandrels.
@set["spandrel HP (BETBG)"] =
{
- rimjoist: 0.170, # * Detail 1.2.7
- parapet: 0.660, # * Detail 1.3.2
- fenestration: 0.000, # * ... generally part of clear-field RSi
- corner: 0.200, # * Detail 1.4.2
- balcony: 0.400, # * Detail 9.1.15
- party: 0.500, # ** ... similar to parapet/balcony
- grade: 0.880, # * Detail 2.5.1
- joint: 0.140, # * Detail 7.4.2
- transition: 0.000
- }.freeze # "good/high performance" curtainwall spandrels
- self.gen("spandrel HP (BETBG)")
+ rimjoist: 0.170000, # Detail 1.2.7
+ parapet: 0.660000, # Detail 1.3.2
+ roof: 0.660000, # same as parapet
+ fenestration: 0.000000, # inferred, generally part of clear-field RSi
+ door: 0.000000, # inferred, generally part of clear-field RSi
+ skylight: 0.350000, # same as "regular (BETBG)"
+ spandrel: 0.155000, # Detail 5.4.4
+ corner: 0.200000, # Detail 1.4.2
+ balcony: 0.400000, # Detail 9.1.15
+ balconysill: 0.400000, # same as balcony
+ balconydoorsill: 0.400000, # same as balconysill
+ party: 0.500000, # inferred, similar to parapet/balcony
+ grade: 0.880000, # Detail 2.5.1
+ joint: 0.140000, # Detail 7.4.2
+ transition: 0.000000 # defaults to 0
+ }.freeze
- @set["code (Quebec)"] = # NECB-QC (code-compliant) defaults:
+ # CCQ, Chapitre I1, code-compliant defaults.
+ @set["code (Quebec)"] =
{
- rimjoist: 0.300, # *
- parapet: 0.325, # *
- fenestration: 0.200, # *
- corner: 0.300, # ** not explicitely stated
- balcony: 0.500, # *
- party: 0.450, # *
- grade: 0.450, # *
- joint: 0.200, # *
- transition: 0.000
+ rimjoist: 0.300000, # re I1
+ parapet: 0.325000, # re I1
+ roof: 0.325000, # same as parapet
+ fenestration: 0.200000, # re I1
+ door: 0.200000, # re I1
+ skylight: 0.200000, # re I1
+ spandrel: 0.155000, # BETBG Detail 5.4.4 (same as uncompliant)
+ corner: 0.300000, # inferred from description, not explicitely set
+ balcony: 0.500000, # re I1
+ balconysill: 0.500000, # same as balcony
+ balconydoorsill: 0.500000, # same as balconysill
+ party: 0.450000, # re I1
+ grade: 0.450000, # re I1
+ joint: 0.200000, # re I1
+ transition: 0.000000 # defaults to 0
}.freeze
- self.gen("code (Quebec)")
- @set["uncompliant (Quebec)"] = # NECB-QC (non-code-compliant) defaults:
+ # CCQ, Chapitre I1, non-code-compliant defaults.
+ @set["uncompliant (Quebec)"] =
{
- rimjoist: 0.850, # *
- parapet: 0.800, # *
- fenestration: 0.500, # *
- corner: 0.850, # ** not explicitely stated
- balcony: 1.000, # *
- party: 0.850, # *
- grade: 0.850, # *
- joint: 0.500, # *
- transition: 0.000
+ rimjoist: 0.850000, # re I1
+ parapet: 0.800000, # re I1
+ roof: 0.800000, # same as parapet
+ fenestration: 0.500000, # re I1
+ door: 0.500000, # re I1
+ skylight: 0.500000, # re I1
+ spandrel: 0.155000, # BETBG Detail 5.4.4 (same as compliant)
+ corner: 0.850000, # inferred from description, not explicitely set
+ balcony: 1.000000, # re I1
+ balconysill: 1.000000, # same as balcony
+ balconydoorsill: 1.000000, # same as balconysill
+ party: 0.850000, # re I1
+ grade: 0.850000, # re I1
+ joint: 0.500000, # re I1
+ transition: 0.000000 # defaults to 0
}.freeze
- self.gen("uncompliant (Quebec)")
- @set["(non thermal bridging)"] = # ... would not derate surfaces:
+ # ASHRAE 90.1 2022 (A10) "default" steel-framed and metal buildings.
+ @set["90.1.22|steel.m|default"] =
{
- rimjoist: 0.000,
- parapet: 0.000,
- fenestration: 0.000,
- corner: 0.000,
- balcony: 0.000,
- party: 0.000,
- grade: 0.000,
- joint: 0.000,
- transition: 0.000
+ rimjoist: 0.307000, # "intermediate floor to wall intersection"
+ parapet: 0.260000, # "parapet" edge
+ roof: 0.020000, # (non-parapet) "roof" edge
+ fenestration: 0.194000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000001, # (unspecified, defaults to 0)
+ corner: 0.000002, # (unspecified, defaults to 0)
+ balcony: 0.307000, # "intermediate floor balcony/overhang" edge
+ balconysill: 0.307000, # "intermediate floor balcony" edge (when sill)
+ balconydoorsill: 0.307000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.376000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
}.freeze
- self.gen("(non thermal bridging)")
+
+ # ASHRAE 90.1 2022 (A10) "unmitigated" steel-framed and metal buildings.
+ @set["90.1.22|steel.m|unmitigated"] =
+ {
+ rimjoist: 0.842000, # "intermediate floor to wall intersection"
+ parapet: 0.500000, # "parapet" edge
+ roof: 0.650000, # (non-parapet) "roof" edge
+ fenestration: 0.505000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000001, # (unspecified, defaults to 0)
+ corner: 0.000002, # (unspecified, defaults to 0)
+ balcony: 0.842000, # "intermediate floor balcony/overhang" edge
+ balconysill: 1.686000, # "intermediate floor balcony" edge (when sill)
+ balconydoorsill: 0.842000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.554000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ # ASHRAE 90.1 2022 (A10) "default" exterior/integral mass walls.
+ @set["90.1.22|mass.ex|default"] =
+ {
+ rimjoist: 0.205000, # "intermediate floor to wall intersection"
+ parapet: 0.217000, # "parapet" edge
+ roof: 0.150000, # (non-parapet) "roof" edge
+ fenestration: 0.226000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000001, # (unspecified, defaults to 0)
+ corner: 0.000002, # (unspecified, defaults to 0)
+ balcony: 0.205000, # "intermediate floor balcony/overhang" edge
+ balconysill: 0.307000, # "intermediate floor balcony" edge (when sill)
+ balconydoorsill: 0.205000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.322000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ # ASHRAE 90.1 2022 (A10) "unmitigated" exterior/integral mass walls.
+ @set["90.1.22|mass.ex|unmitigated"] =
+ {
+ rimjoist: 0.824000, # "intermediate floor to wall intersection"
+ parapet: 0.412000, # "parapet" edge
+ roof: 0.750000, # (non-parapet) "roof" edge
+ fenestration: 0.325000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000001, # (unspecified, defaults to 0)
+ corner: 0.000002, # (unspecified, defaults to 0)
+ balcony: 0.824000, # "intermediate floor balcony/overhang" edge
+ balconysill: 1.686000, # "intermediate floor balcony" edge (when sill)
+ balconydoorsill: 0.824000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.476000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ # ASHRAE 90.1 2022 (A10) "default" interior mass walls.
+ @set["90.1.22|mass.in|default"] =
+ {
+ rimjoist: 0.495000, # "intermediate floor to wall intersection"
+ parapet: 0.393000, # "parapet" edge
+ roof: 0.150000, # (non-parapet) "roof" edge
+ fenestration: 0.143000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000000, # (unspecified, defaults to 0)
+ corner: 0.000001, # (unspecified, defaults to 0)
+ balcony: 0.495000, # "intermediate floor balcony/overhang" edge
+ balconysill: 0.307000, # "intermediate floor balcony" edge (when sill)
+ balconydoorsill: 0.495000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.322000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ # ASHRAE 90.1 2022 (A10) "unmitigated" interior mass walls.
+ @set["90.1.22|mass.in|unmitigated"] =
+ {
+ rimjoist: 0.824000, # "intermediate floor to wall intersection"
+ parapet: 0.884000, # "parapet" edge
+ roof: 0.750000, # (non-parapet) "roof" edge
+ fenestration: 0.543000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000000, # (unspecified, defaults to 0)
+ corner: 0.000001, # (unspecified, defaults to 0)
+ balcony: 0.824000, # "intermediate floor balcony/overhang" edge
+ balconysill: 1.686000, # "intermediate floor balcony" edge (when sill)
+ balconydoorsill: 0.824000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.476000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ # ASHRAE 90.1 2022 (A10) "default" wood-framed (and other) walls.
+ @set["90.1.22|wood.fr|default"] =
+ {
+ rimjoist: 0.084000, # "intermediate floor to wall intersection"
+ parapet: 0.056000, # "parapet" edge
+ roof: 0.020000, # (non-parapet) "roof" edge
+ fenestration: 0.171000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000000, # (unspecified, defaults to 0)
+ corner: 0.000001, # (unspecified, defaults to 0)
+ balcony: 0.084000, # "intermediate floor balcony/overhang" edge
+ balconysill: 0.171001, # same as :fenestration
+ balconydoorsill: 0.084000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.074000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ # ASHRAE 90.1 2022 (A10) "unmitigated" wood-framed (and other) walls.
+ @set["90.1.22|wood.fr|unmitigated"] =
+ {
+ rimjoist: 0.582000, # "intermediate floor to wall intersection"
+ parapet: 0.056000, # "parapet" edge
+ roof: 0.150000, # (non-parapet) "roof" edge
+ fenestration: 0.260000, # "wall to vertical fenestration intersection"
+ door: 0.000000, # (unspecified, defaults to 0)
+ skylight: 0.000000, # (unspecified, defaults to 0)
+ spandrel: 0.000000, # (unspecified, defaults to 0)
+ corner: 0.000001, # (unspecified, defaults to 0)
+ balcony: 0.582000, # same as :rimjoist
+ balconysill: 0.582000, # same as :rimjoist
+ balconydoorsill: 0.582000, # same as balcony
+ party: 0.000001, # (unspecified, defaults to 0)
+ grade: 0.000001, # (unspecified, defaults to 0)
+ joint: 0.322000, # placeholder for "cladding support"
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ @set["(non thermal bridging)"] =
+ {
+ rimjoist: 0.000000, # defaults to 0
+ parapet: 0.000000, # defaults to 0
+ roof: 0.000000, # defaults to 0
+ fenestration: 0.000000, # defaults to 0
+ door: 0.000000, # defaults to 0
+ skylight: 0.000000, # defaults to 0
+ spandrel: 0.000000, # defaults to 0
+ corner: 0.000000, # defaults to 0
+ balcony: 0.000000, # defaults to 0
+ balconysill: 0.000000, # defaults to 0
+ party: 0.000000, # defaults to 0
+ grade: 0.000000, # defaults to 0
+ joint: 0.000000, # defaults to 0
+ transition: 0.000000 # defaults to 0
+ }.freeze
+
+ @set.keys.each { |k| self.gen(k) }
end
##
- # Generate PSI set shorthand listings (requires a valid id).
+ # Generates PSI set shorthand listings.
#
- # @param id [String] a PSI set identifier
+ # @param id PSI set identifier
#
- # @return [Bool] true if successful in generating PSI set shorthands
- # @return [Bool] false if invalid input
+ # @return [Bool] whether successful in generating PSI set shorthands
+ # @return [false] if invalid input (see logs)
def gen(id = "")
mth = "TBD::#{__callee__}"
- a = false
+ return hashkey(id, @set, id, mth, ERR, false) unless @set.key?(id)
- return TBD.mismatch("id", id, String, mth, DBG, a) unless id.is_a?(String)
- return TBD.hashkey(id, @set, id, mth, ERR, a) unless @set.key?(id)
+ h = {} # true/false if PSI set has PSI type
+ h[:joint ] = @set[id].key?(:joint)
+ h[:transition ] = @set[id].key?(:transition)
+ h[:fenestration ] = @set[id].key?(:fenestration)
+ h[:head ] = @set[id].key?(:head)
+ h[:headconcave ] = @set[id].key?(:headconcave)
+ h[:headconvex ] = @set[id].key?(:headconvex)
+ h[:sill ] = @set[id].key?(:sill)
+ h[:sillconcave ] = @set[id].key?(:sillconcave)
+ h[:sillconvex ] = @set[id].key?(:sillconvex)
+ h[:jamb ] = @set[id].key?(:jamb)
+ h[:jambconcave ] = @set[id].key?(:jambconcave)
+ h[:jambconvex ] = @set[id].key?(:jambconvex)
+ h[:door ] = @set[id].key?(:door)
+ h[:doorhead ] = @set[id].key?(:doorhead)
+ h[:doorheadconcave ] = @set[id].key?(:doorheadconcave)
+ h[:doorheadconvex ] = @set[id].key?(:doorheadconvex)
+ h[:doorsill ] = @set[id].key?(:doorsill)
+ h[:doorsillconcave ] = @set[id].key?(:doorsillconcave)
+ h[:doorsillconvex ] = @set[id].key?(:doorsillconvex)
+ h[:doorjamb ] = @set[id].key?(:doorjamb)
+ h[:doorjambconcave ] = @set[id].key?(:doorjambconcave)
+ h[:doorjambconvex ] = @set[id].key?(:doorjambconvex)
+ h[:skylight ] = @set[id].key?(:skylight)
+ h[:skylighthead ] = @set[id].key?(:skylighthead)
+ h[:skylightheadconcave ] = @set[id].key?(:skylightheadconcave)
+ h[:skylightheadconvex ] = @set[id].key?(:skylightheadconvex)
+ h[:skylightsill ] = @set[id].key?(:skylightsill)
+ h[:skylightsillconcave ] = @set[id].key?(:skylightsillconcave)
+ h[:skylightsillconvex ] = @set[id].key?(:skylightsillconvex)
+ h[:skylightjamb ] = @set[id].key?(:skylightjamb)
+ h[:skylightjambconcave ] = @set[id].key?(:skylightjambconcave)
+ h[:skylightjambconvex ] = @set[id].key?(:skylightjambconvex)
+ h[:spandrel ] = @set[id].key?(:spandrel)
+ h[:spandrelconcave ] = @set[id].key?(:spandrelconcave)
+ h[:spandrelconvex ] = @set[id].key?(:spandrelconvex)
+ h[:corner ] = @set[id].key?(:corner)
+ h[:cornerconcave ] = @set[id].key?(:cornerconcave)
+ h[:cornerconvex ] = @set[id].key?(:cornerconvex)
+ h[:party ] = @set[id].key?(:party)
+ h[:partyconcave ] = @set[id].key?(:partyconcave)
+ h[:partyconvex ] = @set[id].key?(:partyconvex)
+ h[:parapet ] = @set[id].key?(:parapet)
+ h[:partyconcave ] = @set[id].key?(:parapetconcave)
+ h[:parapetconvex ] = @set[id].key?(:parapetconvex)
+ h[:roof ] = @set[id].key?(:roof)
+ h[:roofconcave ] = @set[id].key?(:roofconcave)
+ h[:roofconvex ] = @set[id].key?(:roofconvex)
+ h[:grade ] = @set[id].key?(:grade)
+ h[:gradeconcave ] = @set[id].key?(:gradeconcave)
+ h[:gradeconvex ] = @set[id].key?(:gradeconvex)
+ h[:balcony ] = @set[id].key?(:balcony)
+ h[:balconyconcave ] = @set[id].key?(:balconyconcave)
+ h[:balconyconvex ] = @set[id].key?(:balconyconvex)
+ h[:balconysill ] = @set[id].key?(:balconysill)
+ h[:balconysillconcave ] = @set[id].key?(:balconysillconvex)
+ h[:balconysillconvex ] = @set[id].key?(:balconysillconvex)
+ h[:balconydoorsill ] = @set[id].key?(:balconydoorsill)
+ h[:balconydoorsillconcave] = @set[id].key?(:balconydoorsillconvex)
+ h[:balconydoorsillconvex ] = @set[id].key?(:balconydoorsillconvex)
+ h[:rimjoist ] = @set[id].key?(:rimjoist)
+ h[:rimjoistconcave ] = @set[id].key?(:rimjoistconcave)
+ h[:rimjoistconvex ] = @set[id].key?(:rimjoistconvex)
+ @has[id] = h
- h = {} # true/false if PSI set has PSI type
- h[:joint ] = @set[id].key?(:joint )
- h[:transition ] = @set[id].key?(:transition )
- h[:fenestration ] = @set[id].key?(:fenestration )
- h[:head ] = @set[id].key?(:head )
- h[:headconcave ] = @set[id].key?(:headconcave )
- h[:headconvex ] = @set[id].key?(:headconvex )
- h[:sill ] = @set[id].key?(:sill )
- h[:sillconcave ] = @set[id].key?(:sillconcave )
- h[:sillconvex ] = @set[id].key?(:sillconvex )
- h[:jamb ] = @set[id].key?(:jamb )
- h[:jambconcave ] = @set[id].key?(:jambconcave )
- h[:jambconvex ] = @set[id].key?(:jambconvex )
- h[:corner ] = @set[id].key?(:corner )
- h[:cornerconcave ] = @set[id].key?(:cornerconcave )
- h[:cornerconvex ] = @set[id].key?(:cornerconvex )
- h[:parapet ] = @set[id].key?(:parapet )
- h[:partyconcave ] = @set[id].key?(:parapetconcave )
- h[:parapetconvex ] = @set[id].key?(:parapetconvex )
- h[:party ] = @set[id].key?(:party )
- h[:partyconcave ] = @set[id].key?(:partyconcave )
- h[:partyconvex ] = @set[id].key?(:partyconvex )
- h[:grade ] = @set[id].key?(:grade )
- h[:gradeconcave ] = @set[id].key?(:gradeconcave )
- h[:gradeconvex ] = @set[id].key?(:gradeconvex )
- h[:balcony ] = @set[id].key?(:balcony )
- h[:balconyconcave ] = @set[id].key?(:balconyconcave )
- h[:balconyconvex ] = @set[id].key?(:balconyconvex )
- h[:rimjoist ] = @set[id].key?(:rimjoist )
- h[:rimjoistconcave] = @set[id].key?(:rimjoistconcave)
- h[:rimjoistconvex ] = @set[id].key?(:rimjoistconvex )
- @has[id] = h
+ v = {} # PSI-value (W/K per linear meter)
+ v[:door ] = 0; v[:fenestration ] = 0; v[:skylight ] = 0
+ v[:head ] = 0; v[:headconcave ] = 0; v[:headconvex ] = 0
+ v[:sill ] = 0; v[:sillconcave ] = 0; v[:sillconvex ] = 0
+ v[:jamb ] = 0; v[:jambconcave ] = 0; v[:jambconvex ] = 0
+ v[:doorhead ] = 0; v[:doorheadconcave ] = 0; v[:doorconvex ] = 0
+ v[:doorsill ] = 0; v[:doorsillconcave ] = 0; v[:doorsillconvex ] = 0
+ v[:doorjamb ] = 0; v[:doorjambconcave ] = 0; v[:doorjambconvex ] = 0
+ v[:skylighthead ] = 0; v[:skylightheadconcave ] = 0; v[:skylightconvex ] = 0
+ v[:skylightsill ] = 0; v[:skylightsillconcave ] = 0; v[:skylightsillconvex ] = 0
+ v[:skylightjamb ] = 0; v[:skylightjambconcave ] = 0; v[:skylightjambconvex ] = 0
+ v[:spandrel ] = 0; v[:spandrelconcave ] = 0; v[:spandrelconvex ] = 0
+ v[:corner ] = 0; v[:cornerconcave ] = 0; v[:cornerconvex ] = 0
+ v[:parapet ] = 0; v[:parapetconcave ] = 0; v[:parapetconvex ] = 0
+ v[:roof ] = 0; v[:roofconcave ] = 0; v[:roofconvex ] = 0
+ v[:party ] = 0; v[:partyconcave ] = 0; v[:partyconvex ] = 0
+ v[:grade ] = 0; v[:gradeconcave ] = 0; v[:gradeconvex ] = 0
+ v[:balcony ] = 0; v[:balconyconcave ] = 0; v[:balconyconvex ] = 0
+ v[:balconysill ] = 0; v[:balconysillconcave ] = 0; v[:balconysillconvex ] = 0
+ v[:balconydoorsill] = 0; v[:balconydoorsillconcave] = 0; v[:balconydoorsillconvex] = 0
+ v[:rimjoist ] = 0; v[:rimjoistconcave ] = 0; v[:rimjoistconvex ] = 0
+ v[:joint ] = 0; v[:transition ] = 0
- v = {} # PSI-value (W/K per linear meter)
- v[:joint ] = 0; v[:transition ] = 0; v[:fenestration ] = 0
- v[:head ] = 0; v[:headconcave ] = 0; v[:headconvex ] = 0
- v[:sill ] = 0; v[:sillconcave ] = 0; v[:sillconvex ] = 0
- v[:jamb ] = 0; v[:jambconcave ] = 0; v[:jambconvex ] = 0
- v[:corner ] = 0; v[:cornerconcave ] = 0; v[:cornerconvex ] = 0
- v[:parapet ] = 0; v[:parapetconcave ] = 0; v[:parapetconvex ] = 0
- v[:party ] = 0; v[:partyconcave ] = 0; v[:partyconvex ] = 0
- v[:grade ] = 0; v[:gradeconcave ] = 0; v[:gradeconvex ] = 0
- v[:balcony ] = 0; v[:balconyconcave ] = 0; v[:balconyconvex ] = 0
- v[:rimjoist] = 0; v[:rimjoistconcave] = 0; v[:rimjoistconvex] = 0
+ v[:joint ] = @set[id][:joint ] if h[:joint ]
+ v[:transition ] = @set[id][:transition ] if h[:transition ]
+ v[:fenestration ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:head ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:headconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:headconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:sill ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:sillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:sillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:jamb ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:jambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:jambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:door ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorhead ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorheadconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorheadconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorsill ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorsillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorsillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorjamb ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorjambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:doorjambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylight ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylighthead ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightheadconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightheadconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightsill ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightsillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightsillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightjamb ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightjambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:skylightjambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:door ] = @set[id][:door ] if h[:door ]
+ v[:doorhead ] = @set[id][:door ] if h[:door ]
+ v[:doorheadconcave ] = @set[id][:door ] if h[:door ]
+ v[:doorheadconvex ] = @set[id][:door ] if h[:door ]
+ v[:doorsill ] = @set[id][:door ] if h[:door ]
+ v[:doorsillconcave ] = @set[id][:door ] if h[:door ]
+ v[:doorsillconvex ] = @set[id][:door ] if h[:door ]
+ v[:doorjamb ] = @set[id][:door ] if h[:door ]
+ v[:doorjambconcave ] = @set[id][:door ] if h[:door ]
+ v[:doorjambconvex ] = @set[id][:door ] if h[:door ]
+ v[:skylight ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylighthead ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightheadconcave ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightheadconvex ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightsill ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightsillconcave ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightsillconvex ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightjamb ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightjambconcave ] = @set[id][:skylight ] if h[:skylight ]
+ v[:skylightjambconvex ] = @set[id][:skylight ] if h[:skylight ]
+ v[:head ] = @set[id][:head ] if h[:head ]
+ v[:headconcave ] = @set[id][:head ] if h[:head ]
+ v[:headconvex ] = @set[id][:head ] if h[:head ]
+ v[:sill ] = @set[id][:sill ] if h[:sill ]
+ v[:sillconcave ] = @set[id][:sill ] if h[:sill ]
+ v[:sillconvex ] = @set[id][:sill ] if h[:sill ]
+ v[:jamb ] = @set[id][:jamb ] if h[:jamb ]
+ v[:jambconcave ] = @set[id][:jamb ] if h[:jamb ]
+ v[:jambconvex ] = @set[id][:jamb ] if h[:jamb ]
+ v[:doorhead ] = @set[id][:doorhead ] if h[:doorhead ]
+ v[:doorheadconcave ] = @set[id][:doorhead ] if h[:doorhead ]
+ v[:doorheadconvex ] = @set[id][:doorhead ] if h[:doorhead ]
+ v[:doorsill ] = @set[id][:doorsill ] if h[:doorsill ]
+ v[:doorsillconcave ] = @set[id][:doorsill ] if h[:doorsill ]
+ v[:doorsillconvex ] = @set[id][:doorsill ] if h[:doorsill ]
+ v[:doorjamb ] = @set[id][:doorjamb ] if h[:doorjamb ]
+ v[:doorjambconcave ] = @set[id][:doorjamb ] if h[:doorjamb ]
+ v[:doorjambconvex ] = @set[id][:doorjamb ] if h[:doorjamb ]
+ v[:skylighthead ] = @set[id][:skylighthead ] if h[:skylighthead ]
+ v[:skylightheadconcave ] = @set[id][:skylighthead ] if h[:skylighthead ]
+ v[:skylightheadconvex ] = @set[id][:skylighthead ] if h[:skylighthead ]
+ v[:skylightsill ] = @set[id][:skylightsill ] if h[:skylightsill ]
+ v[:skylightsillconcave ] = @set[id][:skylightsill ] if h[:skylightsill ]
+ v[:skylightsillconvex ] = @set[id][:skylightsill ] if h[:skylightsill ]
+ v[:skylightjamb ] = @set[id][:skylightjamb ] if h[:skylightjamb ]
+ v[:skylightjambconcave ] = @set[id][:skylightjamb ] if h[:skylightjamb ]
+ v[:skylightjambconvex ] = @set[id][:skylightjamb ] if h[:skylightjamb ]
+ v[:headconcave ] = @set[id][:headconcave ] if h[:headconcave ]
+ v[:headconvex ] = @set[id][:headconvex ] if h[:headconvex ]
+ v[:sillconcave ] = @set[id][:sillconcave ] if h[:sillconcave ]
+ v[:sillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
+ v[:jambconcave ] = @set[id][:jambconcave ] if h[:jambconcave ]
+ v[:jambconvex ] = @set[id][:jambconvex ] if h[:jambconvex ]
+ v[:doorheadconcave ] = @set[id][:doorheadconcave ] if h[:doorheadconcave ]
+ v[:doorheadconvex ] = @set[id][:doorheadconvex ] if h[:doorheadconvex ]
+ v[:doorsillconcave ] = @set[id][:doorsillconcave ] if h[:doorsillconcave ]
+ v[:doorsillconvex ] = @set[id][:doorsillconvex ] if h[:doorsillconvex ]
+ v[:doorjambconcave ] = @set[id][:doorjambconcave ] if h[:doorjambconcave ]
+ v[:doorjambconvex ] = @set[id][:doorjambconvex ] if h[:doorjambconvex ]
+ v[:skylightheadconcave ] = @set[id][:skylightheadconcave ] if h[:skylightheadconcave ]
+ v[:skylightheadconvex ] = @set[id][:skylightheadconvex ] if h[:skylightheadconvex ]
+ v[:skylightsillconcave ] = @set[id][:skylightsillconcave ] if h[:skylightsillconcave ]
+ v[:skylightsillconvex ] = @set[id][:skylightsillconvex ] if h[:skylightsillconvex ]
+ v[:skylightjambconcave ] = @set[id][:skylightjambconcave ] if h[:skylightjambconcave ]
+ v[:skylightjambconvex ] = @set[id][:skylightjambconvex ] if h[:skylightjambconvex ]
+ v[:spandrel ] = @set[id][:spandrel ] if h[:spandrel ]
+ v[:spandrelconcave ] = @set[id][:spandrel ] if h[:spandrel ]
+ v[:spandrelconvex ] = @set[id][:spandrel ] if h[:spandrel ]
+ v[:spandrelconcave ] = @set[id][:spandrelconcave ] if h[:spandrelconcave ]
+ v[:spandrelconvex ] = @set[id][:spandrelconvex ] if h[:spandrelconvex ]
+ v[:corner ] = @set[id][:corner ] if h[:corner ]
+ v[:cornerconcave ] = @set[id][:corner ] if h[:corner ]
+ v[:cornerconvex ] = @set[id][:corner ] if h[:corner ]
+ v[:cornerconcave ] = @set[id][:cornerconcave ] if h[:cornerconcave ]
+ v[:cornerconvex ] = @set[id][:cornerconvex ] if h[:cornerconvex ]
+ v[:parapet ] = @set[id][:roof ] if h[:roof ]
+ v[:parapetconcave ] = @set[id][:roof ] if h[:roof ]
+ v[:parapetconvex ] = @set[id][:roof ] if h[:roof ]
+ v[:parapetconcave ] = @set[id][:roofconcave ] if h[:roofconcave ]
+ v[:parapetconvex ] = @set[id][:roofconvex ] if h[:roofconvex ]
+ v[:parapet ] = @set[id][:parapet ] if h[:parapet ]
+ v[:parapetconcave ] = @set[id][:parapet ] if h[:parapet ]
+ v[:parapetconvex ] = @set[id][:parapet ] if h[:parapet ]
+ v[:parapetconcave ] = @set[id][:parapetconcave ] if h[:parapetconcave ]
+ v[:parapetconvex ] = @set[id][:parapetconvex ] if h[:parapetconvex ]
+ v[:roof ] = @set[id][:parapet ] if h[:parapet ]
+ v[:roofconcave ] = @set[id][:parapet ] if h[:parapet ]
+ v[:roofconvex ] = @set[id][:parapet ] if h[:parapet ]
+ v[:roofconcave ] = @set[id][:parapetconcave ] if h[:parapetconcave ]
+ v[:roofconvex ] = @set[id][:parapetxonvex ] if h[:parapetconvex ]
+ v[:roof ] = @set[id][:roof ] if h[:roof ]
+ v[:roofconcave ] = @set[id][:roof ] if h[:roof ]
+ v[:roofconvex ] = @set[id][:roof ] if h[:roof ]
+ v[:roofconcave ] = @set[id][:roofconcave ] if h[:roofconcave ]
+ v[:roofconvex ] = @set[id][:roofconvex ] if h[:roofconvex ]
+ v[:party ] = @set[id][:party ] if h[:party ]
+ v[:partyconcave ] = @set[id][:party ] if h[:party ]
+ v[:partyconvex ] = @set[id][:party ] if h[:party ]
+ v[:partyconcave ] = @set[id][:partyconcave ] if h[:partyconcave ]
+ v[:partyconvex ] = @set[id][:partyconvex ] if h[:partyconvex ]
+ v[:grade ] = @set[id][:grade ] if h[:grade ]
+ v[:gradeconcave ] = @set[id][:grade ] if h[:grade ]
+ v[:gradeconvex ] = @set[id][:grade ] if h[:grade ]
+ v[:gradeconcave ] = @set[id][:gradeconcave ] if h[:gradeconcave ]
+ v[:gradeconvex ] = @set[id][:gradeconvex ] if h[:gradeconvex ]
+ v[:balcony ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconyconcave ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconyconvex ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconyconcave ] = @set[id][:balconyconcave ] if h[:balconyconcave ]
+ v[:balconyconvex ] = @set[id][:balconyconvex ] if h[:balconyconvex ]
+ v[:balconysill ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:balconysillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:balconysillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:balconydoorsill ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:balconydoorsillconcave] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:balconydoorsillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
+ v[:balconysill ] = @set[id][:sill ] if h[:sill ]
+ v[:balconysillconcave ] = @set[id][:sill ] if h[:sill ]
+ v[:balconysillconvex ] = @set[id][:sill ] if h[:sill ]
+ v[:balconysillconcave ] = @set[id][:sillconcave ] if h[:sillconcave ]
+ v[:balconysillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
+ v[:balconydoorsill ] = @set[id][:sill ] if h[:sill ]
+ v[:balconydoorsillconcave] = @set[id][:sill ] if h[:sill ]
+ v[:balconydoorsillconvex ] = @set[id][:sill ] if h[:sill ]
+ v[:balconydoorsillconcave] = @set[id][:sillconcave ] if h[:sillconcave ]
+ v[:balconydoorsillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
+ v[:balconysill ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconysillconcave ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconysillconvex ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconysillconcave ] = @set[id][:balconyconcave ] if h[:balconyconcave ]
+ v[:balconysillconvex ] = @set[id][:balconyconvex ] if h[:balconycinvex ]
+ v[:balconydoorsill ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconydoorsillconcave] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconydoorsillconvex ] = @set[id][:balcony ] if h[:balcony ]
+ v[:balconydoorsillconcave] = @set[id][:balconyconcave ] if h[:balconyconcave ]
+ v[:balconydoorsillconvex ] = @set[id][:balconyconvex ] if h[:balconycinvex ]
+ v[:balconysill ] = @set[id][:balconysill ] if h[:balconysill ]
+ v[:balconysillconcave ] = @set[id][:balconysill ] if h[:balconysill ]
+ v[:balconysillconvex ] = @set[id][:balconysill ] if h[:balconysill ]
+ v[:balconysillconcave ] = @set[id][:balconysillconcave ] if h[:balconysillconcave ]
+ v[:balconysillconvex ] = @set[id][:balconysillconvex ] if h[:balconysillconvex ]
+ v[:balconydoorsill ] = @set[id][:balconysill ] if h[:balconysill ]
+ v[:balconydoorsillconcave] = @set[id][:balconysill ] if h[:balconysill ]
+ v[:balconydoorsillconvex ] = @set[id][:balconysill ] if h[:balconysill ]
+ v[:balconydoorsillconcave] = @set[id][:balconysillconcave ] if h[:balconysillconcave ]
+ v[:balconydoorsillconvex ] = @set[id][:balconysillconvex ] if h[:balconysillconvex ]
+ v[:balconydoorsill ] = @set[id][:balconydoorsill ] if h[:balconydoorsill ]
+ v[:balconydoorsillconcave] = @set[id][:balconydoorsill ] if h[:balconydoorsill ]
+ v[:balconydoorsillconvex ] = @set[id][:balconydoorsill ] if h[:balconydoorsill ]
+ v[:balconydoorsillconcave] = @set[id][:balconydoorsillconcave] if h[:balconydoorsillconcave]
+ v[:balconydoorsillconvex ] = @set[id][:balconydoorsillconvex ] if h[:balconydoorsillconvex ]
+ v[:rimjoist ] = @set[id][:rimjoist ] if h[:rimjoist ]
+ v[:rimjoistconcave ] = @set[id][:rimjoist ] if h[:rimjoist ]
+ v[:rimjoistconvex ] = @set[id][:rimjoist ] if h[:rimjoist ]
+ v[:rimjoistconcave ] = @set[id][:rimjoistconcave ] if h[:rimjoistconcave ]
+ v[:rimjoistconvex ] = @set[id][:rimjoistconvex ] if h[:rimjoistconvex ]
- v[:joint ] = @set[id][:joint ] if h[:joint ]
- v[:transition ] = @set[id][:transition ] if h[:transition ]
- v[:fenestration ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:head ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:headconcave ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:headconvex ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:sill ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:sillconcave ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:sillconvex ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:jamb ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:jambconcave ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:jambconvex ] = @set[id][:fenestration ] if h[:fenestration ]
- v[:head ] = @set[id][:head ] if h[:head ]
- v[:headconcave ] = @set[id][:head ] if h[:head ]
- v[:headconvex ] = @set[id][:head ] if h[:head ]
- v[:sill ] = @set[id][:sill ] if h[:sill ]
- v[:sillconcave ] = @set[id][:sill ] if h[:sill ]
- v[:sillconvex ] = @set[id][:sill ] if h[:sill ]
- v[:jamb ] = @set[id][:jamb ] if h[:jamb ]
- v[:jambconcave ] = @set[id][:jamb ] if h[:jamb ]
- v[:jambconvex ] = @set[id][:jamb ] if h[:jamb ]
- v[:headconcave ] = @set[id][:headconcave ] if h[:headconcave ]
- v[:headconvex ] = @set[id][:headconvex ] if h[:headconvex ]
- v[:sillconcave ] = @set[id][:sillconcave ] if h[:sillconcave ]
- v[:sillconvex ] = @set[id][:sillconvex ] if h[:sillconvex ]
- v[:jambconcave ] = @set[id][:jambconcave ] if h[:jambconcave ]
- v[:jambconvex ] = @set[id][:jambconvex ] if h[:jambconvex ]
- v[:corner ] = @set[id][:corner ] if h[:corner ]
- v[:cornerconcave ] = @set[id][:corner ] if h[:corner ]
- v[:cornerconvex ] = @set[id][:corner ] if h[:corner ]
- v[:cornerconcave ] = @set[id][:cornerconcave ] if h[:cornerconcave ]
- v[:cornerconvex ] = @set[id][:cornerconvex ] if h[:cornerconvex ]
- v[:parapet ] = @set[id][:parapet ] if h[:parapet ]
- v[:parapetconcave ] = @set[id][:parapet ] if h[:parapet ]
- v[:parapetconvex ] = @set[id][:parapet ] if h[:parapet ]
- v[:parapetconcave ] = @set[id][:parapetconcave ] if h[:parapetconcave ]
- v[:parapetconvex ] = @set[id][:parapetconvex ] if h[:parapetconvex ]
- v[:party ] = @set[id][:party ] if h[:party ]
- v[:partyconcave ] = @set[id][:party ] if h[:party ]
- v[:partyconvex ] = @set[id][:party ] if h[:party ]
- v[:partyconcave ] = @set[id][:partyconcave ] if h[:partyconcave ]
- v[:partyconvex ] = @set[id][:partyconvex ] if h[:partyconvex ]
- v[:grade ] = @set[id][:grade ] if h[:grade ]
- v[:gradeconcave ] = @set[id][:grade ] if h[:grade ]
- v[:gradeconvex ] = @set[id][:grade ] if h[:grade ]
- v[:gradeconcave ] = @set[id][:gradeconcave ] if h[:gradeconcave ]
- v[:gradeconvex ] = @set[id][:gradeconvex ] if h[:gradeconvex ]
- v[:balcony ] = @set[id][:balcony ] if h[:balcony ]
- v[:balconyconcave ] = @set[id][:balcony ] if h[:balcony ]
- v[:balconyconvex ] = @set[id][:balcony ] if h[:balcony ]
- v[:balconyconcave ] = @set[id][:balconyconcave ] if h[:balconyconcave ]
- v[:balconyconvex ] = @set[id][:balconyconvex ] if h[:balconyconvex ]
- v[:rimjoist ] = @set[id][:rimjoist ] if h[:rimjoist ]
- v[:rimjoistconcave] = @set[id][:rimjoist ] if h[:rimjoist ]
- v[:rimjoistconvex ] = @set[id][:rimjoist ] if h[:rimjoist ]
- v[:rimjoistconcave] = @set[id][:rimjoistconcave] if h[:rimjoistconcave]
- v[:rimjoistconvex ] = @set[id][:rimjoistconvex ] if h[:rimjoistconvex ]
-
max = [v[:parapetconcave], v[:parapetconvex]].max
v[:parapet] = max unless @has[:parapet]
+
+ max = [v[:roofconcave], v[:roofconvex]].max
+ v[:roof] = max unless @has[:roof]
@val[id] = v
true
end
##
- # Append a new PSI set, based on a TBD JSON-formatted PSI set object -
- # requires a valid, unique :id.
+ # Appends a new PSI set.
#
- # @param set [Hash] a new PSI set
+ # @param [Hash] set a new PSI set
+ # @option set [#to_s] :id PSI set identifier
+ # @option set [#to_f] :rimjoist intermediate floor-to-wall intersection
+ # @option set [#to_f] :rimjoistconcave basilaire variant
+ # @option set [#to_f] :rimjoistconvex cantilever variant
+ # @option set [#to_f] :parapet roof-to-wall intersection
+ # @option set [#to_f] :parapetconcave basilaire variant
+ # @option set [#to_f] :parapetconvex typical
+ # @option set [#to_f] :roof roof-to-wall intersection
+ # @option set [#to_f] :roofconcave basilaire variant
+ # @option set [#to_f] :roofconvex typical
+ # @option set [#to_f] :fenestration head/sill/jamb interface
+ # @option set [#to_f] :head (fenestrated) header interface
+ # @option set [#to_f] :headconcave (fenestrated) basilaire variant
+ # @option set [#to_f] :headconvex (fenestrated) parapet variant
+ # @option set [#to_f] :sill (fenestrated) threshold/sill interface
+ # @option set [#to_f] :sillconcave (fenestrated) basilaire variant
+ # @option set [#to_f] :sillconvex (fenestrated) cantilever variant
+ # @option set [#to_f] :jamb (fenestrated) side jamb interface
+ # @option set [#to_f] :jambconcave (fenestrated) interior corner variant
+ # @option set [#to_f] :jambconvex (fenestrated) exterior corner variant
+ # @option set [#to_f] :door (opaque) head/sill/jamb interface
+ # @option set [#to_f] :doorhead (opaque) header interface
+ # @option set [#to_f] :doorheadconcave (opaque) basilaire variant
+ # @option set [#to_f] :doorheadconvex (opaque) parapet variant
+ # @option set [#to_f] :doorsill (opaque) threshold interface
+ # @option set [#to_f] :doorsillconcave (opaque) basilaire variant
+ # @option set [#to_f] :doorsillconvex (opaque) cantilever variant
+ # @option set [#to_f] :doorjamb (opaque) side jamb interface
+ # @option set [#to_f] :doorjambconcave (opaque) interior corner variant
+ # @option set [#to_f] :doorjambconvex (opaque) exterior corner variant
+ # @option set [#to_f] :skylight to roof interface
+ # @option set [#to_f] :skylighthead header interface
+ # @option set [#to_f] :skylightheadconcave basilaire variant
+ # @option set [#to_f] :skylightheadconvex parapet variant
+ # @option set [#to_f] :skylightsill sill interface
+ # @option set [#to_f] :skylightsillconcave basilaire variant
+ # @option set [#to_f] :skylightsillconvex cantilever variant
+ # @option set [#to_f] :skylightjamb side jamb interface
+ # @option set [#to_f] :skylightjambconcave (opaque) interior corner variant
+ # @option set [#to_f] :skylightjambconvex (opaque) parapet variant
+ # @option set [#to_f] :spandrel spandrel/other interface
+ # @option set [#to_f] :spandrelconcave interior corner variant
+ # @option set [#to_f] :spandrelconvex exterior corner variant
+ # @option set [#to_f] :corner corner intersection
+ # @option set [#to_f] :cornerconcave interior corner variant
+ # @option set [#to_f] :cornerconvex exterior corner variant
+ # @option set [#to_f] :balcony intermediate floor-balcony intersection
+ # @option set [#to_f] :balconyconcave basilaire variant
+ # @option set [#to_f] :balconyconvex cantilever variant
+ # @option set [#to_f] :balconysill intermediate floor-balcony-fenestration intersection
+ # @option set [#to_f] :balconysilloncave basilaire variant
+ # @option set [#to_f] :balconysillconvex cantilever variant
+ # @option set [#to_f] :balconydoorsill intermediate floor-balcony-door intersection
+ # @option set [#to_f] :balconydoorsilloncave basilaire variant
+ # @option set [#to_f] :balconydoorsillconvex cantilever variant
+ # @option set [#to_f] :party demising surface intersection
+ # @option set [#to_f] :partyconcave interior corner or basilaire variant
+ # @option set [#to_f] :partyconvex exterior corner or cantilever variant
+ # @option set [#to_f] :grade foundation wall or slab-on-grade intersection
+ # @option set [#to_f] :gradeconcave cantilever variant
+ # @option set [#to_f] :gradeconvex basilaire variant
+ # @option set [#to_f] :joint strong ~coplanar joint
+ # @option set [#to_f] :transition mild ~coplanar transition
#
- # @return [Bool] true if successfully appended
- # @return [Bool] false if invalid input
+ # @return [Bool] whether PSI set is successfully appended
+ # @return [false] if invalid input (see logs)
def append(set = {})
mth = "TBD::#{__callee__}"
a = false
+ s = {}
+ return mismatch("set" , set, Hash, mth, DBG, a) unless set.is_a?(Hash)
+ return hashkey("set id", set, :id , mth, DBG, a) unless set.key?(:id)
- return TBD.mismatch("set", set, Hash, mth, DBG, a) unless set.is_a?(Hash)
- return TBD.hashkey("set id", set, :id, mth, DBG, a) unless set.key?(:id)
+ id = trim(set[:id])
+ return mismatch("set ID", set[:id], String, mth, ERR, a) if id.empty?
- exists = @set.key?(set[:id])
- TBD.log(ERR, "'#{set[:id]}': existing PSI set (#{mth})") if exists
- return false if exists
+ if @set.key?(id)
+ log(ERR, "'#{id}': existing PSI set (#{mth})")
+ return a
+ end
- s = {}
# Most PSI types have concave and convex variants, depending on the polar
# position of deratable surfaces about an edge-as-thermal-bridge. One
# exception is :fenestration, which TBD later breaks down into :head,
# :sill or :jamb edge types. Another exception is a :joint edge: a PSI
# type that is not autoassigned to an edge (i.e., only via a TBD JSON
# input file). Finally, transitions are autoassigned by TBD when an edge
# is "flat", i.e, no noticeable polar angle difference between surfaces.
- s[:rimjoist ] = set[:rimjoist ] if set.key?(:rimjoist )
- s[:rimjoistconcave] = set[:rimjoistconcave] if set.key?(:rimjoistconcave)
- s[:rimjoistconvex ] = set[:rimjoistconvex ] if set.key?(:rimjoistconvex )
- s[:parapet ] = set[:parapet ] if set.key?(:parapet )
- s[:parapetconcave ] = set[:parapetconcave ] if set.key?(:parapetconcave )
- s[:parapetconvex ] = set[:parapetconvex ] if set.key?(:parapetconvex )
- s[:head ] = set[:head ] if set.key?(:head )
- s[:headconcave ] = set[:headconcave ] if set.key?(:headconcave )
- s[:headconvex ] = set[:headconvex ] if set.key?(:headconvex )
- s[:sill ] = set[:sill ] if set.key?(:sill )
- s[:sillconcave ] = set[:sillconcave ] if set.key?(:sillconcave )
- s[:sillconvex ] = set[:sillconvex ] if set.key?(:sillconvex )
- s[:jamb ] = set[:jamb ] if set.key?(:jamb )
- s[:jambconcave ] = set[:jambconcave ] if set.key?(:jambconcave )
- s[:jambconvex ] = set[:jambconvex ] if set.key?(:jambconcave )
- s[:corner ] = set[:corner ] if set.key?(:corner )
- s[:cornerconcave ] = set[:cornerconcave ] if set.key?(:cornerconcave )
- s[:cornerconvex ] = set[:cornerconvex ] if set.key?(:cornerconvex )
- s[:balcony ] = set[:balcony ] if set.key?(:balcony )
- s[:balconyconcave ] = set[:balconyconcave ] if set.key?(:balconyconcave )
- s[:balconyconvex ] = set[:balconyconvex ] if set.key?(:balconyconvex )
- s[:party ] = set[:party ] if set.key?(:party )
- s[:partyconcave ] = set[:partyconcave ] if set.key?(:partyconcave )
- s[:partyconvex ] = set[:partyconvex ] if set.key?(:partyconvex )
- s[:grade ] = set[:grade ] if set.key?(:grade )
- s[:gradeconcave ] = set[:gradeconcave ] if set.key?(:gradeconcave )
- s[:gradeconvex ] = set[:gradeconvex ] if set.key?(:gradeconvex )
- s[:fenestration ] = set[:fenestration ] if set.key?(:fenestration )
- s[:joint ] = set[:joint ] if set.key?(:joint )
- s[:transition ] = set[:transition ] if set.key?(:transition )
+ s[:rimjoist ] = set[:rimjoist ] if set.key?(:rimjoist)
+ s[:rimjoistconcave ] = set[:rimjoistconcave ] if set.key?(:rimjoistconcave)
+ s[:rimjoistconvex ] = set[:rimjoistconvex ] if set.key?(:rimjoistconvex)
+ s[:parapet ] = set[:parapet ] if set.key?(:parapet)
+ s[:parapetconcave ] = set[:parapetconcave ] if set.key?(:parapetconcave)
+ s[:parapetconvex ] = set[:parapetconvex ] if set.key?(:parapetconvex)
+ s[:roof ] = set[:roof ] if set.key?(:roof)
+ s[:roofconcave ] = set[:roofconcave ] if set.key?(:roofconcave)
+ s[:roofconvex ] = set[:roofconvex ] if set.key?(:roofconvex)
+ s[:fenestration ] = set[:fenestration ] if set.key?(:fenestration)
+ s[:head ] = set[:head ] if set.key?(:head)
+ s[:headconcave ] = set[:headconcave ] if set.key?(:headconcave)
+ s[:headconvex ] = set[:headconvex ] if set.key?(:headconvex)
+ s[:sill ] = set[:sill ] if set.key?(:sill)
+ s[:sillconcave ] = set[:sillconcave ] if set.key?(:sillconcave)
+ s[:sillconvex ] = set[:sillconvex ] if set.key?(:sillconvex)
+ s[:jamb ] = set[:jamb ] if set.key?(:jamb)
+ s[:jambconcave ] = set[:jambconcave ] if set.key?(:jambconcave)
+ s[:jambconvex ] = set[:jambconvex ] if set.key?(:jambconvex)
+ s[:door ] = set[:door ] if set.key?(:door)
+ s[:doorhead ] = set[:doorhead ] if set.key?(:doorhead)
+ s[:doorheadconcave ] = set[:doorheadconcave ] if set.key?(:doorheadconcave)
+ s[:doorheadconvex ] = set[:doorheadconvex ] if set.key?(:doorheadconvex)
+ s[:doorsill ] = set[:doorsill ] if set.key?(:doorsill)
+ s[:doorsillconcave ] = set[:doorsillconcave ] if set.key?(:doorsillconcave)
+ s[:doorsillconvex ] = set[:doorsillconvex ] if set.key?(:doorsillconvex)
+ s[:doorjamb ] = set[:doorjamb ] if set.key?(:doorjamb)
+ s[:doorjambconcave ] = set[:doorjambconcave ] if set.key?(:doorjambconcave)
+ s[:doorjambconvex ] = set[:doorjambconvex ] if set.key?(:doorjambconvex)
+ s[:skylight ] = set[:skylight ] if set.key?(:skylight)
+ s[:skylighthead ] = set[:skylighthead ] if set.key?(:skylighthead)
+ s[:skylightheadconcave ] = set[:skylightheadconcave ] if set.key?(:skylightheadconcave)
+ s[:skylightheadconvex ] = set[:skylightheadconvex ] if set.key?(:skylightheadconvex)
+ s[:skylightsill ] = set[:skylightsill ] if set.key?(:skylightsill)
+ s[:skylightsillconcave ] = set[:skylightsillconcave ] if set.key?(:skylightsillconcave)
+ s[:skylightsillconvex ] = set[:skylightsillconvex ] if set.key?(:skylightsillconvex)
+ s[:skylightjamb ] = set[:skylightjamb ] if set.key?(:skylightjamb)
+ s[:skylightjambconcave ] = set[:skylightjambconcave ] if set.key?(:skylightjambconcave)
+ s[:skylightjambconvex ] = set[:skylightjambconvex ] if set.key?(:skylightjambconvex)
+ s[:spandrel ] = set[:spandrel ] if set.key?(:spandrel)
+ s[:spandrelconcave ] = set[:spandrelconcave ] if set.key?(:spandrelconcave)
+ s[:spandrelconvex ] = set[:spandrelconvex ] if set.key?(:spandrelconvex)
+ s[:corner ] = set[:corner ] if set.key?(:corner)
+ s[:cornerconcave ] = set[:cornerconcave ] if set.key?(:cornerconcave)
+ s[:cornerconvex ] = set[:cornerconvex ] if set.key?(:cornerconvex)
+ s[:balcony ] = set[:balcony ] if set.key?(:balcony)
+ s[:balconyconcave ] = set[:balconyconcave ] if set.key?(:balconyconcave)
+ s[:balconyconvex ] = set[:balconyconvex ] if set.key?(:balconyconvex)
+ s[:balconysill ] = set[:balconysill ] if set.key?(:balconysill)
+ s[:balconysillconcave ] = set[:balconysillconcave ] if set.key?(:balconysillconcave)
+ s[:balconysillconvex ] = set[:balconysillconvex ] if set.key?(:balconysillconvex)
+ s[:balconydoorsill ] = set[:balconydoorsill ] if set.key?(:balconydoorsill)
+ s[:balconydoorsillconcave] = set[:balconydoorsillconcave] if set.key?(:balconydoorsillconcave)
+ s[:balconydoorsillconvex ] = set[:balconydoorsillconvex ] if set.key?(:balconydoorsillconvex)
+ s[:party ] = set[:party ] if set.key?(:party)
+ s[:partyconcave ] = set[:partyconcave ] if set.key?(:partyconcave)
+ s[:partyconvex ] = set[:partyconvex ] if set.key?(:partyconvex)
+ s[:grade ] = set[:grade ] if set.key?(:grade)
+ s[:gradeconcave ] = set[:gradeconcave ] if set.key?(:gradeconcave)
+ s[:gradeconvex ] = set[:gradeconvex ] if set.key?(:gradeconvex)
+ s[:joint ] = set[:joint ] if set.key?(:joint)
+ s[:transition ] = set[:transition ] if set.key?(:transition)
- s[:joint ] = 0.000 unless set.key?(:joint )
- s[:transition ] = 0.000 unless set.key?(:transition )
+ s[:joint ] = 0.000 unless set.key?(:joint)
+ s[:transition ] = 0.000 unless set.key?(:transition)
- @set[set[:id]] = s
- self.gen(set[:id])
+ @set[id] = s
+ self.gen(id)
true
end
##
- # Return PSI set shorthands. The return Hash holds 2x keys ... has: a Hash
- # of true/false (values) for any admissible PSI type (keys), and val: a Hash
- # of PSI-values for any admissible PSI type (default: 0.0 W/K per meter).
+ # Returns PSI set shorthands. The return Hash holds 2 keys, has: a Hash
+ # of true/false (values) for any admissible PSI type (keys), and val: a
+ # Hash of PSI-factors (values) for any admissible PSI type (keys).
+ # PSI-factors default to 0 W/K per linear meter if missing from set.
#
- # @param id [String] a PSI set identifier
+ # @param id [#to_s] PSI set identifier
+ # @example intermediate floor slab intersection
+ # shorthands("A901")
#
- # @return [Hash] has: Hash of true/false, val: Hash of PSI values
- # @return [Hash] has: empty Hash, val: empty Hash (if invalid/missing set)
+ # @return [Hash] has: Hash (Bool), val: Hash (PSI factors) see logs if empty
def shorthands(id = "")
mth = "TBD::#{__callee__}"
- cl = String
sh = { has: {}, val: {} }
+ id = trim(id)
+ return mismatch("set ID", id, String, mth, ERR, a) if id.empty?
+ return hashkey(id, @set , id, mth, ERR, sh) unless @set.key?(id)
+ return hashkey(id, @has , id, mth, ERR, sh) unless @has.key?(id)
+ return hashkey(id, @val , id, mth, ERR, sh) unless @val.key?(id)
- return TBD.mismatch("id", id, String, mth, DBG, sh) unless id.is_a?(cl)
- return TBD.hashkey(id, @set, id, mth, ERR, sh) unless @set.key?(id)
- return TBD.hashkey(id, @has, id, mth, ERR, sh) unless @has.key?(id)
- return TBD.hashkey(id, @val, id, mth, ERR, sh) unless @val.key?(id)
-
sh[:has] = @has[id]
sh[:val] = @val[id]
sh
end
##
- # Validate whether a given PSI set has a complete list of PSI type:values.
+ # Validates whether a given PSI set has a complete list of PSI type:values.
#
- # @param id [String] a PSI set identifier
+ # @param id [#to_s] PSI set identifier
#
- # @return [Bool] true if found and is complete
- # @return [Bool] false if invalid input
+ # @return [Bool] whether provided PSI set is held in memory and is complete
+ # @return [false] if invalid input (see logs)
def complete?(id = "")
mth = "TBD::#{__callee__}"
a = false
+ id = trim(id)
+ return mismatch("set ID", id, String, mth, ERR, a) if id.empty?
+ return hashkey(id, @set , id, mth, ERR, a) unless @set.key?(id)
+ return hashkey(id, @has , id, mth, ERR, a) unless @has.key?(id)
+ return hashkey(id, @val , id, mth, ERR, a) unless @val.key?(id)
- return TBD.mismatch("id", id, String, mth, DBG, a) unless id.is_a?(String)
- return TBD.hashkey(id, @set, id, mth, ERR, a) unless @set.key?(id)
- return TBD.hashkey(id, @has, id, mth, ERR, a) unless @has.key?(id)
- return TBD.hashkey(id, @val, id, mth, ERR, a) unless @val.key?(id)
-
holes = []
- holes << :head if @has[id][:head ]
- holes << :sill if @has[id][:sill ]
- holes << :jamb if @has[id][:jamb ]
+ holes << :head if @has[id][:head ]
+ holes << :sill if @has[id][:sill ]
+ holes << :jamb if @has[id][:jamb ]
ok = holes.size == 3
- ok = true if @has[id][:fenestration ]
- return false unless ok
+ ok = true if @has[id][:fenestration ]
+ return false unless ok
corners = []
- corners << :concave if @has[id][:cornerconcave ]
- corners << :convex if @has[id][:cornerconvex ]
+ corners << :concave if @has[id][:cornerconcave ]
+ corners << :convex if @has[id][:cornerconvex ]
ok = corners.size == 2
- ok = true if @has[id][:corner ]
- return false unless ok
+ ok = true if @has[id][:corner ]
+ return false unless ok
parapets = []
- parapets << :concave if @has[id][:parapetconcave]
- parapets << :convex if @has[id][:parapetconvex ]
- ok = parapets.size == 2
- ok = true if @has[id][:parapet ]
- return false unless ok
- return false unless @has[id][:party ]
- return false unless @has[id][:grade ]
- return false unless @has[id][:balcony ]
- return false unless @has[id][:rimjoist ]
+ roofs = []
+ parapets << :concave if @has[id][:parapetconcave]
+ parapets << :convex if @has[id][:parapetconvex ]
+ roofs << :concave if @has[id][:roofconcave ]
+ parapets << :convex if @has[id][:roofconvex ]
+ ok = parapets.size == 2 || roofs.size == 2
+ ok = true if @has[id][:parapet ]
+ ok = true if @has[id][:roof ]
+ return false unless ok
+ return false unless @has[id][:party ]
+ return false unless @has[id][:grade ]
+ return false unless @has[id][:balcony ]
+ return false unless @has[id][:rimjoist ]
ok
end
##
- # Return safe PSI type if missing input from PSI set (based on inheritance).
+ # Returns safe PSI type if missing from PSI set (based on inheritance).
#
- # @param id [String] a PSI set identifier
- # @param type [Symbol] a PSI type, e.g. :rimjoistconcave
+ # @param id [#to_s] PSI set identifier
+ # @param type [#to_sym] PSI type
+ # @example intermediate floor slab intersection
+ # safe("90.1.22|wood.fr|unmitigated", :rimjoistconcave)
#
# @return [Symbol] safe PSI type
- # @return [Nil] if invalid input or no safe PSI type found
+ # @return [nil] if invalid inputs (see logs)
def safe(id = "", type = nil)
mth = "TBD::#{__callee__}"
- cl1 = String
- cl2 = Symbol
+ id = trim(id)
+ ck1 = id.empty?
+ ck2 = type.respond_to?(:to_sym)
+ return mismatch("set ID", id, String, mth) if ck1
+ return mismatch("type", type, Symbol, mth) unless ck2
+ return hashkey(id, @set, id, mth, ERR) unless @set.key?(id)
+ return hashkey(id, @has, id, mth, ERR) unless @has.key?(id)
- return TBD.mismatch("id", id, cl1, mth) unless id.is_a?(cl1)
- return TBD.mismatch("type", type, cl2, mth, ERR) unless type.is_a?(cl2)
- return TBD.hashkey(id, @set, id, mth, ERR) unless @set.key?(id)
- return TBD.hashkey(id, @has, id, mth, ERR) unless @has.key?(id)
+ safer = type.to_sym
- safer = type
+ unless @has[id][safer]
+ concave = safer.to_s.include?("concave")
+ convex = safer.to_s.include?("convex")
+ safer = safer.to_s.chomp("concave").to_sym if concave
+ safer = safer.to_s.chomp("convex").to_sym if convex
+ end
unless @has[id][safer]
- concave = type.to_s.include?("concave")
- convex = type.to_s.include?("convex")
- safer = type.to_s.chomp("concave").to_sym if concave
- safer = type.to_s.chomp("convex").to_sym if convex
+ safer = :fenestration if safer == :head
+ safer = :fenestration if safer == :sill
+ safer = :fenestration if safer == :jamb
+ safer = :door if safer == :doorhead
+ safer = :door if safer == :doorsill
+ safer = :door if safer == :doorjamb
+ safer = :skylight if safer == :skylighthead
+ safer = :skylight if safer == :skylightsill
+ safer = :skylight if safer == :skylightjamb
+ end
- unless @has[id][safer]
- safer = :fenestration if safer == :head
- safer = :fenestration if safer == :sill
- safer = :fenestration if safer == :jamb
- end
+ unless @has[id][safer]
+ safer = :fenestration if safer == :skylight
+ safer = :fenestration if safer == :door
end
return safer if @has[id][safer]
nil
end
end
##
- # Process TBD JSON inputs, after TBD has processed OpenStudio model variables
- # and retrieved corresponding Topolys model surface/edge properties. TBD user
- # inputs allow customization of default assumptions and inferred values.
- # If successful, "edges" (input) may inherit additional properties, e.g.:
- # edge-specific PSI set (defined in TBD JSON file), edge-specific PSI type
- # (e.g. "corner", defined in TBD JSON file), project-wide PSI set (if absent
- # from TBD JSON file).
+ # Processes TBD JSON inputs, after TBD has preprocessed OpenStudio model
+ # variables and retrieved corresponding Topolys model surface/edge
+ # properties. TBD user inputs allow customization of default assumptions and
+ # inferred values. If successful, "edges" (input) may inherit additional
+ # properties, e.g.: edge-specific PSI set (defined in TBD JSON file),
+ # edge-specific PSI type (e.g. "corner", defined in TBD JSON file),
+ # project-wide PSI set (if absent from TBD JSON file).
#
- # @param s [Hash] preprocessed TBD surfaces
- # @param e [Hash] preprocessed TBD edges
- # @param argh [Hash] arguments
+ # @param [Hash] s TBD surfaces (keys: Openstudio surface names)
+ # @option s [Hash] :windows TBD surface-specific windows e.g. s[][:windows]
+ # @option s [Hash] :doors TBD surface-specific doors
+ # @option s [Hash] :skylights TBD surface-specific skylights
+ # @option s [OpenStudio::Model::BuildingStory] :story OpenStudio story
+ # @option s ["Wall", "RoofCeiling", "Floor"] :stype OpenStudio surface type
+ # @option s [OpenStudio::Model::Space] :space OpenStudio space
+ # @param [Hash] e TBD edges (keys: Topolys edge identifiers)
+ # @option e [Hash] :surfaces linked TBD surfaces e.g. e[][:surfaces]
+ # @option e [#to_f] :length edge length in m
+ # @option e [Topolys::Point3D] :v0 origin vertex
+ # @option e [Topolys::Point3D] :v1 terminal vertex
+ # @param [Hash] argh TBD arguments
+ # @option argh [#to_s] :option selected PSI set
+ # @option argh [#to_s] :io_path tbd.json input file path
+ # @option argh [#to_s] :schema_path TBD JSON schema file path
#
- # @return [Hash] io: JSON inputs (Hash), psi:/khi: new (enriched) sets (Hash)
- # @return [Hash] io: empty Hash if invalid input
+ # @return [Hash] io: (Hash), psi:/khi: enriched sets (see logs if empty)
def inputs(s = {}, e = {}, argh = {})
mth = "TBD::#{__callee__}"
opt = :option
ipt = { io: {}, psi: PSI.new, khi: KHI.new }
io = {}
+ return mismatch("s" , s , Hash, mth, DBG, ipt) unless s.is_a?(Hash)
+ return mismatch("e" , e , Hash, mth, DBG, ipt) unless e.is_a?(Hash)
+ return mismatch("argh", argh, Hash, mth, DBG, ipt) unless argh.is_a?(Hash)
+ return hashkey("argh" , argh, opt , mth, DBG, ipt) unless argh.key?(opt)
- return mismatch("s", s, Hash, mth, DBG, ipt) unless s.is_a?(Hash)
- return mismatch("e", s, Hash, mth, DBG, ipt) unless e.is_a?(Hash)
- return mismatch("argh", s, Hash, mth, DBG, ipt) unless argh.is_a?(Hash)
- return hashkey("argh", argh, opt, mth, DBG, ipt) unless argh.key?(opt)
-
argh[:io_path ] = nil unless argh.key?(:io_path)
argh[:schema_path] = nil unless argh.key?(:schema_path)
pth = argh[:io_path ]
sch = argh[:schema_path]
@@ -578,63 +1106,67 @@
if pth && (pth.is_a?(String) || pth.is_a?(Hash))
if pth.is_a?(Hash)
io = pth
else
return empty("JSON file", mth, FTL, ipt) unless File.size?(pth)
+
io = File.read(pth)
io = JSON.parse(io, symbolize_names: true)
return mismatch("io", io, Hash, mth, FTL, ipt) unless io.is_a?(Hash)
end
# Schema validation is not yet supported in the OpenStudio Application.
- # We nonetheless recommend that users rely on the json-schema gem, or an
- # online linter, prior to using TBD. The following checks focus on content
- # - ignoring bad JSON input otherwise caught via JSON validation.
+ # It is nonetheless recommended that users rely on the json-schema gem,
+ # or an online linter, prior to using TBD. The following checks focus on
+ # content - ignoring bad JSON input otherwise caught via JSON validation.
#
# A side note: JSON validation relies on case-senitive string comparisons
# (e.g. OpenStudio space or surface names, vs corresponding TBD JSON
- # identifiers). So "Space-1" doesn't match "SPACE-1" - head's up.
+ # identifiers). So "Space-1" doesn't match "SPACE-1" ... head's up!
if sch
require "json-schema"
+ return invalid("JSON schema", mth, 3, FTL, ipt) unless File.exist?(sch)
+ return empty("JSON schema" , mth, FTL, ipt) if File.zero?(sch)
- return invalid("JSON schema", mth, 0, FTL, ipt) unless File.exist?(sch)
- return empty("JSON schema", mth, FTL, ipt) if File.zero?(sch)
schema = File.read(sch)
schema = JSON.parse(schema, symbolize_names: true)
valid = JSON::Validator.validate!(schema, io)
- return invalid("JSON schema validation", mth, 0, FTL, ipt) unless valid
+ return invalid("JSON schema validation", mth, 3, FTL, ipt) unless valid
end
# Append JSON entries to library of linear & point thermal bridges.
- io[:psis].each { |psi| ipt[:psi].append(psi) } if io.key?(:psis)
- io[:khis].each { |khi| ipt[:khi].append(khi) } if io.key?(:khis)
+ io[:psis].each { |psi| ipt[:psi].append(psi) } if io.key?(:psis)
+ io[:khis].each { |khi| ipt[:khi].append(khi) } if io.key?(:khis)
# JSON-defined or user-selected, building PSI set must be complete/valid.
io[:building] = { psi: argh[opt] } unless io.key?(:building)
bdg = io[:building]
- ok = bdg.key?(:psi)
- return hashkey("Building PSI", bdg, :psi, mth, FTL, ipt) unless ok
+ ok = bdg.key?(:psi)
+ return hashkey("Building PSI", bdg, :psi, mth, FTL, ipt) unless ok
+
ok = ipt[:psi].complete?(bdg[:psi])
- return invalid("Complete building PSI", mth, 0, FTL, ipt) unless ok
+ return invalid("Complete building PSI", mth, 3, FTL, ipt) unless ok
# Validate remaining (optional) JSON entries.
[:stories, :spacetypes, :spaces].each do |types|
key = :story
key = :stype if types == :spacetypes
key = :space if types == :spaces
if io.key?(types)
io[types].each do |type|
next unless type.key?(:psi)
- next unless type.key?(:id)
+ next unless type.key?(:id )
+
s1 = "JSON/OSM '#{type[:id]}' (#{mth})"
s2 = "JSON/PSI '#{type[:id]}' set (#{mth})"
match = false
- s.values.each do |props| # TBD model surface linked to type?
- break if match
+ s.values.each do |props| # TBD surface linked to type?
+ break if match
next unless props.key?(key)
+
match = type[:id] == props[key].nameString
end
log(ERR, s1) unless match
log(ERR, s2) unless ipt[:psi].set.key?(type[:psi])
@@ -643,10 +1175,11 @@
end
if io.key?(:surfaces)
io[:surfaces].each do |surface|
next unless surface.key?(:id)
+
s1 = "JSON/OSM surface '#{surface[:id]}' (#{mth})"
log(ERR, s1) unless s.key?(surface[:id])
# surfaces can OPTIONALLY hold custom PSI sets and/or KHI data
if surface.key?(:psi)
@@ -655,10 +1188,11 @@
end
if surface.key?(:khis)
surface[:khis].each do |khi|
next unless khi.key?(:id)
+
s3 = "JSON/KHI surface '#{surface[:id]}' '#{khi[:id]}' (#{mth})"
log(ERR, s3) unless ipt[:khi].point.key?(khi[:id])
end
end
end
@@ -666,19 +1200,21 @@
if io.key?(:subsurfaces)
io[:subsurfaces].each do |sub|
next unless sub.key?(:id)
next unless sub.key?(:usi)
+
match = false
s.each do |id, surface|
break if match
[:windows, :doors, :skylights].each do |holes|
if surface.key?(holes)
surface[holes].keys.each do |id|
break if match
+
match = sub[:id] == id
end
end
end
end
@@ -689,44 +1225,46 @@
if io.key?(:edges)
io[:edges].each do |edge|
next unless edge.key?(:type)
next unless edge.key?(:surfaces)
- surfaces = edge[:surfaces]
- type = edge[:type].to_sym
- safer = ipt[:psi].safe(bdg[:psi], type) # fallback
+
+ surfaces = edge[:surfaces]
+ type = edge[:type].to_sym
+ safer = ipt[:psi].safe(bdg[:psi], type) # fallback
log(ERR, "Skipping invalid edge PSI '#{type}' (#{mth})") unless safer
next unless safer
+
valid = true
- surfaces.each do |surface| # TBD edge's surfaces on file
- e.values.each do |ee| # TBD edges in memory
- break unless valid # if previous anomaly detected
- next if ee.key?(:io_type) # validated from previous loop
+ surfaces.each do |surface| # TBD edge's surfaces on file
+ e.values.each do |ee| # TBD edges in memory
+ break unless valid # if previous anomaly detected
+ next if ee.key?(:io_type) # validated from previous loop
next unless ee.key?(:surfaces)
+
surfs = ee[:surfaces]
next unless surfs.key?(surface)
# An edge on file is valid if ALL of its listed surfaces together
- # connect at least one or more TBD/Topolys model edges in memory.
- # Each of the latter may connect e.g. 3x TBD/Topolys surfaces,
- # but the list of surfaces on file may be shorter, e.g. only 2x.
+ # connect at least 1 or more TBD/Topolys model edges in memory.
+ # Each of the latter may connect e.g. 3 TBD/Topolys surfaces,
+ # but the list of surfaces on file may be shorter, e.g. only 2.
match = true
surfaces.each { |id| match = false unless surfs.key?(id) }
next unless match
- if edge.key?(:length) # optional
+ if edge.key?(:length) # optional
next unless (ee[:length] - edge[:length]).abs < TOL
end
# Optionally, edge coordinates may narrow down potential matches.
if edge.key?(:v0x) || edge.key?(:v0y) || edge.key?(:v0z) ||
edge.key?(:v1x) || edge.key?(:v1y) || edge.key?(:v1z)
unless edge.key?(:v0x) && edge.key?(:v0y) && edge.key?(:v0z) &&
edge.key?(:v1x) && edge.key?(:v1y) && edge.key?(:v1z)
-
log(ERR, "Mismatch '#{surface}' edge vertices (#{mth})")
valid = false
next
end
@@ -741,21 +1279,21 @@
e2[:v0] = ee[:v0].point
e2[:v1] = ee[:v1].point
next unless matches?(e1, e2)
end
- if edge.key?(:psi) # optional
+ if edge.key?(:psi) # optional
set = edge[:psi]
if ipt[:psi].set.key?(set)
saferr = ipt[:psi].safe(set, type)
- ee[:io_set ] = set if saferr
- ee[:io_type] = type if saferr
- log(ERR, "Invalid '#{set}': '#{type}' (#{mth})") unless saferr
- valid = false unless saferr
+ ee[:io_set ] = set if saferr
+ ee[:io_type] = type if saferr
+ log(ERR, "Invalid #{set}: #{type} (#{mth})") unless saferr
+ valid = false unless saferr
else
- log(ERR, "Missing edge PSI '#{set}' (#{mth})")
+ log(ERR, "Missing edge PSI #{set} (#{mth})")
valid = false
end
else
ee[:io_type] = type # success: matching edge - setting edge type
end
@@ -765,325 +1303,339 @@
end
else
# No (optional) user-defined TBD JSON input file. In such cases, provided
# argh[:option] must refer to a valid PSI set. If valid, all edges inherit
# a default PSI set (without KHI entries).
- ok = ipt[:psi].complete?(argh[opt])
- io[:building] = { psi: argh[opt] } if ok
- log(FTL, "Incomplete building PSI set '#{argh[opt]}' (#{mth})") unless ok
- return ipt unless ok
+ msg = "Incomplete building PSI set '#{argh[opt]}' (#{mth})"
+ ok = ipt[:psi].complete?(argh[opt])
+
+ io[:building] = { psi: argh[opt] } if ok
+ log(FTL, msg) unless ok
+ return ipt unless ok
end
ipt[:io] = io
ipt
end
##
- # Thermally derate insulating material within construction.
+ # Thermally derates insulating material within construction.
#
- # @param model [OpenStudio::Model::Model] a model
- # @param id [String] surface identifier
- # @param surface [Hash] a TBD surface
+ # @param id [#to_s] 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 [: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
# @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
#
# @return [OpenStudio::Model::Material] derated (cloned) material
- # @return [NilClass] if invalid input
- def derate(model = nil, id = "", s = {}, lc = nil)
+ # @return [nil] if invalid input (see logs)
+ def derate(id = "", s = {}, lc = nil)
mth = "TBD::#{__callee__}"
m = nil
- k1 = :heatloss
- k2 = :ltype
- k3 = :construction
- k4 = :index
- cl1 = OpenStudio::Model::Model
- cl2 = OpenStudio::Model::LayeredConstruction
- cl3 = Numeric
- cl4 = Symbol
- cl5 = Integer
+ 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
- return mismatch("model", model, cl, mth) unless model.is_a?(cl1)
- return mismatch("id", id, String, mth) unless id.is_a?(String)
- return mismatch(id, s, Hash, mth) unless s.is_a?(Hash)
- return mismatch("lc", lc, Hash, mth) unless lc.is_a?(cl2)
- return hashkey("'#{id}' W/K", s, k1, mth) unless s.key?(k1)
- return invalid("'#{id}' W/K", mth, 3) unless s[k1]
- return mismatch("'#{id}' W/K", s[k1], cl3, mth) unless s[k1].is_a?(cl3)
- return zero("'#{id}' W/K", mth, WRN) if s[k1].abs < TOL
- return hashkey("'#{id}' m2", s, :net, mth) unless s.key?(:net)
- return invalid("'#{id}' m2", mth, 3) unless s[:net]
- return mismatch("'#{id}' m2", s[:net], cl3, mth) unless s[:net].is_a?(cl3)
- return zero("'#{id}' m2", mth, WRN) if s[:net].abs < TOL
- return hashkey("'#{id}' type", s, k2, mth) unless s.key?(k2)
- return invalid("'#{id}' type", mth, 3) unless s[k2]
- return mismatch("'#{id}' type", s[k2], cl4, mth) unless s[k2].is_a?(cl4)
+ kys.each do |k|
+ tag = "#{id} #{k}"
+ return hashkey(tag, s, k, mth, ERR) unless s.key?(k)
- ok = s[k2] == :massless || s[k2] == :standard
+ 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
+ next if [:massless, :standard].include?(s[k])
+ return invalid(tag, mth, 2, ERR)
+ end
+ end
- return invalid("'#{id}' type", mth, 3) unless ok
- return hashkey("'#{id}' construction", s, k3, mth) unless s.key?(k3)
- return hashkey("'#{id}' index", s, k4, mth) unless s.key?(k4)
- return invalid("'#{id}' index", mth, 3) unless s[k4]
- return mismatch("'#{id}' index", s[k4], cl5, mth) unless s[k4].is_a?(cl5)
- return negative("'#{id}' index", mth) if s[k4] < 0
- return hashkey("'#{id}' Rsi", s, :r, mth) unless s.key?(:r)
- return invalid("'#{id}' Rsi", mth, 3) unless s[:r]
- return mismatch("'#{id}' Rsi", s[:r], cl3, mth) unless s[:r].is_a?(cl3)
- return zero("'#{id}' Rsi", mth, WRN) if s[:r].abs < 0.001
+ if lc.nameString.downcase.include?(" tbd")
+ log(WRN, "Won't derate '#{id}': tagged as derated (#{mth})")
+ return m
+ end
- derated = lc.nameString.include?(" tbd")
- log(WRN, "Won't derate '#{id}': already derated (#{mth})") if derated
- return m if derated
-
- index = s[:index]
- ltype = s[:ltype]
- r = s[:r]
- u = s[:heatloss] / s[:net]
+ 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
loss = 0
- de_u = 1 / r + u # derated U
- de_r = 1 / de_u # derated R
+ de_u = 1 / r + u # derated U
+ de_r = 1 / de_u # derated R
if ltype == :massless
m = lc.getLayer(index).to_MasslessOpaqueMaterial
- return invalid("'#{id}' massless layer?", mth, 0) if m.empty?
+ return invalid("#{id} massless layer?", mth, 0) if m.empty?
m = m.get
up = ""
- up = "uprated " if m.nameString.include?(" uprated")
+ 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) * s[:net] unless de_r > 0.001
+ 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)
else
m = lc.getLayer(index).to_StandardOpaqueMaterial
- return invalid("'#{id}' standard layer?", mth, 0) if m.empty?
+ return invalid("#{id} standard layer?", mth, 0) if m.empty?
m = m.get
up = ""
- up = "uprated " if m.nameString.include?(" uprated")
+ 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) * s[:net] unless k < 3
+ k = 3 unless k < 3
+ loss = (de_u - k / d) * net unless k < 3
end
- else # de_r < 0.001 m2.K/W
+ 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) * s[:net]
+ d = 0.003 unless d > 0.003
+ k = d / 0.001 unless d > 0.003
+ loss = (de_u - k / d) * net
end
m.setThickness(d)
m.setThermalConductivity(k)
end
if m && loss > TOL
s[:r_heatloss] = loss
- h_loss = format "%.3f", s[:r_heatloss]
- log(WRN, "Won't assign #{h_loss} W/K to '#{id}': too conductive (#{mth})")
+ hl = format "%.3f", s[:r_heatloss]
+ log(WRN, "Won't assign #{hl} W/K to '#{id}': too conductive (#{mth})")
end
m
end
##
- # Process TBD objects, based on OpenStudio model (OSM) and Topolys model,
- # and derate admissible envelope surfaces by substituting insulating material
- # within surface multilayered constructions with derated clones. Returns a
- # hash holding 2x key:value pairs ... io: objects for JSON serialization and
- # surfaces: derated TBD surfaces.
+ # Processes TBD objects, based on an OpenStudio and generated Topolys model,
+ # and derates admissible envelope surfaces by substituting insulating
+ # materials with derated clones, within surface multilayered constructions.
+ # Returns a Hash holding 2 key:value pairs; io: objects for JSON
+ # serialization, and surfaces: derated TBD surfaces (see exit method).
#
# @param model [OpenStudio::Model::Model] a model
- # @param argh [Hash] TBD arguments
+ # @param [Hash] argh TBD arguments
+ # @option argh [#to_s] :option selected PSI set
+ # @option argh [#to_s] :io_path tbd.json input file path
+ # @option argh [#to_s] :schema_path TBD JSON schema file path
+ # @option argh [Bool] :parapet (true) wall-roof edge as parapet
+ # @option argh [Bool] :uprate_walls whether to uprate walls
+ # @option argh [Bool] :uprate_roofs whether to uprate roofs
+ # @option argh [Bool] :uprate_floors whether to uprate floors
+ # @option argh [Bool] :wall_ut uprated wall Ut target in W/m2•K
+ # @option argh [Bool] :roof_ut uprated roof Ut target in W/m2•K
+ # @option argh [Bool] :floor_ut uprated floor Ut target in W/m2•K
+ # @option argh [#to_s] :wall_option wall construction to uprate (or "all")
+ # @option argh [#to_s] :roof_option roof construction to uprate (or "all")
+ # @option argh [#to_s] :floor_option floor construction to uprate (or "all")
+ # @option argh [Bool] :gen_ua whether to generate a UA' report
+ # @option argh [#to_s] :ua_ref selected UA' ruleset
+ # @option argh [Bool] :gen_kiva whether to generate KIVA inputs
+ # @option argh [#to_f] :sub_tol proximity tolerance between edges in m
#
# @return [Hash] io: (Hash), surfaces: (Hash)
- # @return [Hash] io: nil, surfaces: nil (if invalid input)
+ # @return [Hash] io: nil, surfaces: nil if invalid input (see logs)
def process(model = nil, argh = {})
mth = "TBD::#{__callee__}"
cl = OpenStudio::Model::Model
tbd = { io: nil, surfaces: {} }
-
return mismatch("model", model, cl, mth, DBG, tbd) unless model.is_a?(cl)
return mismatch("argh", argh, Hash, mth, DBG, tbd) unless argh.is_a?(Hash)
- argh = {} if argh.empty?
- argh[:sub_tol ] = TBD::TOL unless argh.key?(:sub_tol )
- argh[:option ] = "" unless argh.key?(:option )
- argh[:io_path ] = nil unless argh.key?(:io_path )
- argh[:schema_path ] = nil unless argh.key?(:schema_path )
- 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 ] = 0 unless argh.key?(:wall_ut )
- argh[:roof_ut ] = 0 unless argh.key?(:roof_ut )
- argh[:floor_ut ] = 0 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 )
- argh[:gen_ua ] = false unless argh.key?(:gen_ua )
- argh[:ua_ref ] = "" unless argh.key?(:ua_ref )
- argh[:gen_kiva ] = false unless argh.key?(:gen_kiva )
+ argh = {} if argh.empty?
+ argh[:option ] = "" unless argh.key?(:option)
+ argh[:io_path ] = nil unless argh.key?(:io_path)
+ argh[:schema_path ] = nil unless argh.key?(:schema_path)
+ argh[:parapet ] = true unless argh.key?(:parapet)
+ 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 ] = 0 unless argh.key?(:wall_ut)
+ argh[:roof_ut ] = 0 unless argh.key?(:roof_ut)
+ argh[:floor_ut ] = 0 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)
+ argh[:gen_ua ] = false unless argh.key?(:gen_ua)
+ argh[:ua_ref ] = "" unless argh.key?(:ua_ref)
+ argh[:gen_kiva ] = false unless argh.key?(:gen_kiva)
+ argh[:reset_kiva ] = false unless argh.key?(:reset_kiva)
+ argh[:sub_tol ] = TBD::TOL unless argh.key?(:sub_tol)
+ # Ensure true or false: whether to generate KIVA inputs.
+ unless [true, false].include?(argh[:gen_kiva])
+ return invalid("generate KIVA option", mth, 0, DBG, tbd)
+ end
+
+ # Ensure true or false: whether to first purge (existing) KIVA inputs.
+ unless [true, false].include?(argh[:reset_kiva])
+ return invalid("reset KIVA option", mth, 0, DBG, tbd)
+ end
+
# Create the Topolys Model.
t_model = Topolys::Model.new
# "true" if any space/zone holds valid setpoint temperatures. With invalid
# inputs, these 2x methods return "false", ignoring any
# setpoint-based logic, e.g. semi-heated spaces (DEBUG errors are logged).
- setpoints = heatingTemperatureSetpoints?(model)
- setpoints = coolingTemperatureSetpoints?(model) || setpoints
+ heated = heatingTemperatureSetpoints?(model)
+ cooled = coolingTemperatureSetpoints?(model)
+ argh[:setpoints] = heated || cooled
- # "true" if any space/zone is part of an HVAC air loop. With invalid inputs,
- # the method returns "false", ignoring any air-loop related logic, e.g.
- # plenum zones as HVAC objects (DEBUG errors are logged).
- airloops = airLoopsHVAC?(model)
-
model.getSurfaces.sort_by { |s| s.nameString }.each do |s|
- # Fetch key attributes of opaque surfaces. Method returns nil with invalid
- # input (DEBUG and ERROR messages may be logged). TBD ignores them.
- surface = properties(model, s)
- next if surface.nil?
+ # Fetch key attributes of opaque surfaces (and any linked sub surfaces).
+ # Method returns nil with invalid input (see logs); TBD ignores them.
+ surface = properties(s, argh)
+ tbd[:surfaces][s.nameString] = surface unless surface.nil?
+ end
- # Similar to "setpoints?" methods above, the boolean methods below also
- # return "false" with invalid inputs, ignoring any space/zone
- # conditioning-based logic (e.g. semi-heated spaces, mislabelling a
- # plenum as an unconditioned zone).
- if setpoints
- if surface[:space].thermalZone.empty?
- plenum = plenum?(surface[:space], airloops, setpoints)
- surface[:conditioned] = false unless plenum
- else
- zone = surface[:space].thermalZone.get
- heat = maxHeatScheduledSetpoint(zone)
- cool = minCoolScheduledSetpoint(zone)
-
- unless heat[:spt] || cool[:spt]
- plenum = plenum?(surface[:space], airloops, setpoints)
- heat[:spt] = 21 if plenum
- cool[:spt] = 24 if plenum
- surface[:conditioned] = false unless plenum
- end
-
- free = heat[:spt] && heat[:spt] < -40 && cool[:spt] && cool[:spt] > 40
- surface[:conditioned] = false if free
- end
- end
-
- # Recover if valid setpoints.
- surface[:heating] = heat[:spt] if heat && heat[:spt]
- surface[:cooling] = cool[:spt] if cool && cool[:spt]
-
- tbd[:surfaces][s.nameString] = surface
- end # (opaque) surfaces populated
-
return empty("TBD surfaces", mth, ERR, tbd) if tbd[:surfaces].empty?
# TBD only derates constructions of opaque surfaces in CONDITIONED spaces,
# ... if facing outdoors or facing UNENCLOSED/UNCONDITIONED spaces.
tbd[:surfaces].each do |id, surface|
surface[:deratable] = false
-
next unless surface[:conditioned]
- next if surface[:ground]
+ next if surface[:ground ]
unless surface[:boundary].downcase == "outdoors"
next unless tbd[:surfaces].key?(surface[:boundary])
- next if tbd[:surfaces][surface[:boundary]][:conditioned]
+ next if tbd[:surfaces][surface[:boundary]][:conditioned]
end
- ok = surface.key?(:index)
- surface[:deratable] = true if ok
- log(ERR, "Skipping '#{id}': insulating layer? (#{mth})") unless ok
+ if surface.key?(:index)
+ surface[:deratable] = true
+ else
+ log(ERR, "Skipping '#{id}': insulating layer? (#{mth})")
+ end
end
- [:windows, :doors, :skylights].each do |holes| # sort kids
+ # Sort subsurfaces before processing.
+ [:windows, :doors, :skylights].each do |holes|
tbd[:surfaces].values.each do |surface|
- ok = surface.key?(holes)
- surface[holes] = surface[holes].sort_by { |_, s| s[:minz] }.to_h if ok
+ next unless surface.key?(holes)
+
+ surface[holes] = surface[holes].sort_by { |_, s| s[:minz] }.to_h
end
end
# Split "surfaces" hash into "floors", "ceilings" and "walls" hashes.
- floors = tbd[:surfaces].select { |_, s| s[:type] == :floor }
- ceilings = tbd[:surfaces].select { |_, s| s[:type] == :ceiling }
- walls = tbd[:surfaces].select { |_, s| s[:type] == :wall }
- floors = floors.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
- ceilings = ceilings.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
- walls = walls.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
+ floors = tbd[:surfaces].select { |_, s| s[:type] == :floor }
+ ceilings = tbd[:surfaces].select { |_, s| s[:type] == :ceiling }
+ walls = tbd[:surfaces].select { |_, s| s[:type] == :wall }
+ floors = floors.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
+ ceilings = ceilings.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
+ walls = walls.sort_by { |_, s| [s[:minz], s[:space]] }.to_h
+
# Fetch OpenStudio shading surfaces & key attributes.
shades = {}
model.getShadingSurfaces.each do |s|
- id = s.nameString
- empty = s.shadingSurfaceGroup.empty?
- log(ERR, "Can't process '#{id}' transformation (#{mth})") if empty
- next if empty
- group = s.shadingSurfaceGroup.get
- shading = group.to_ShadingSurfaceGroup
- tr = transforms(model, group)
- ok = tr[:t] && tr[:r]
- t = tr[:t]
- log(FTL, "Can't process '#{id}' transformation (#{mth})") unless ok
- return tbd unless ok
+ id = s.nameString
+ group = s.shadingSurfaceGroup
+ log(ERR, "Can't process '#{id}' transformation (#{mth})") if group.empty?
+ next if group.empty?
- unless shading.empty?
- empty = shading.get.space.empty?
- tr[:r] += shading.get.space.get.directionofRelativeNorth unless empty
- end
+ group = group.get
+ tr = transforms(group)
+ t = tr[:t] if tr[:t] && tr[:r]
- n = trueNormal(s, tr[:r])
- log(FTL, "Can't process '#{id}' true normal (#{mth})") unless n
- return tbd unless n
+ log(FTL, "Can't process '#{id}' transformation (#{mth})") unless t
+ return tbd unless t
+ space = group.space
+ tr[:r] += space.get.directionofRelativeNorth unless space.empty?
+ n = trueNormal(s, tr[:r])
+ log(FTL, "Can't process '#{id}' true normal (#{mth})") unless n
+ return tbd unless n
+
points = (t * s.vertices).map { |v| Topolys::Point3D.new(v.x, v.y, v.z) }
+
minz = ( points.map { |p| p.z } ).min
- shades[id] = { group: group, points: points, minz: minz, n: n }
- end # shading surfaces populated
+ shades[id] = { group: group, points: points, minz: minz, n: n }
+ end
+
# Mutually populate TBD & Topolys surfaces. Keep track of created "holes".
holes = {}
- floor_holes = dads(t_model, floors )
+ floor_holes = dads(t_model, floors)
ceiling_holes = dads(t_model, ceilings)
- wall_holes = dads(t_model, walls )
+ wall_holes = dads(t_model, walls)
- holes.merge!(floor_holes )
+ holes.merge!(floor_holes)
holes.merge!(ceiling_holes)
- holes.merge!(wall_holes )
+ holes.merge!(wall_holes)
dads(t_model, shades)
# Loop through Topolys edges and populate TBD edge hash. Initially, there
# should be a one-to-one correspondence between Topolys and TBD edge
# objects. Use Topolys-generated identifiers as unique edge hash keys.
edges = {}
- holes.each do |id, wire| # start with hole edges
+ # Start with hole edges.
+ holes.each do |id, wire|
wire.edges.each do |e|
- i = e.id
- l = e.length
- ok = edges.key?(i)
- edges[i] = { length: l, v0: e.v0, v1: e.v1, surfaces: {} } unless ok
- ok = edges[i][:surfaces].key?(wire.attributes[:id])
- edges[i][:surfaces][wire.attributes[:id]] = { wire: wire.id } unless ok
+ i = e.id
+ l = e.length
+ ex = edges.key?(i)
+
+ edges[i] = { length: l, v0: e.v0, v1: e.v1, surfaces: {} } unless ex
+
+ next if edges[i][:surfaces].key?(wire.attributes[:id])
+
+ edges[i][:surfaces][wire.attributes[:id]] = { wire: wire.id }
end
end
# Next, floors, ceilings & walls; then shades.
- faces(floors, edges )
+ faces(floors , edges)
faces(ceilings, edges)
- faces(walls, edges )
- faces(shades, edges )
+ faces(walls , edges)
+ faces(shades , edges)
+ # Purge existing KIVA objects from model.
+ if argh[:reset_kiva]
+ kva = false
+ kva = true unless model.getSurfacePropertyExposedFoundationPerimeters.empty?
+ kva = true unless model.getFoundationKivas.empty?
+
+ if kva
+ if argh[:gen_kiva]
+ resetKIVA(model, "Foundation")
+ else
+ resetKIVA(model, "Ground")
+ end
+ end
+ end
+
# Generate OSM Kiva settings and objects if foundation-facing floors.
- # returns false if partial failure (log failure eventually).
+ # Returns false if partial failure (log failure eventually).
kiva(model, walls, floors, edges) if argh[:gen_kiva]
# Thermal bridging characteristics of edges are determined - in part - by
# relative polar position of linked surfaces (or wires) around each edge.
# This attribute is key in distinguishing concave from convex edges.
@@ -1111,34 +1663,37 @@
dz = (origin.z - terminal.z).abs
horizontal = dz.abs < TOL
vertical = dx < TOL && dy < TOL
edge_V = terminal - origin
- invalid("1x edge length < TOL", mth, 0, ERROR) if edge_V.magnitude < TOL
- next if edge_V.magnitude < TOL
+ if edge_V.magnitude < TOL
+ invalid("1x edge length < TOL", mth, 0, ERROR)
+ next
+ end
edge_plane = Topolys::Plane3D.new(origin, edge_V)
if vertical
reference_V = north.dup
elsif horizontal
reference_V = zenith.dup
- else # project zenith vector unto edge plane
+ else # project zenith vector unto edge plane
reference = edge_plane.project(origin + zenith)
reference_V = reference - origin
end
edge[:surfaces].each do |id, surface|
# Loop through each linked wire and determine farthest point from
# edge while ensuring candidate point is not aligned with edge.
t_model.wires.each do |wire|
- next unless surface[:wire] == wire.id # should be a unique match
+ next unless surface[:wire] == wire.id # should be a unique match
+
normal = tbd[:surfaces][id][:n] if tbd[:surfaces].key?(id)
normal = holes[id].attributes[:n] if holes.key?(id)
normal = shades[id][:n] if shades.key?(id)
farthest = Topolys::Point3D.new(origin.x, origin.y, origin.z)
- farthest_V = farthest - origin # zero magnitude, initially
+ farthest_V = farthest - origin # zero magnitude, initially
inverted = false
i_origin = wire.points.index(origin)
i_terminal = wire.points.index(terminal)
i_last = wire.points.size - 1
@@ -1165,49 +1720,52 @@
plane = Topolys::Plane3D.from_points(terminal, origin, point)
else
plane = Topolys::Plane3D.from_points(origin, terminal, point)
end
- next unless (normal.x - plane.normal.x).abs < TOL &&
- (normal.y - plane.normal.y).abs < TOL &&
- (normal.z - plane.normal.z).abs < TOL
+ dnx = (normal.x - plane.normal.x).abs
+ dny = (normal.y - plane.normal.y).abs
+ dnz = (normal.z - plane.normal.z).abs
+ next unless dnx < TOL && dny < TOL && dnz < TOL
farther = point_V_magnitude > farthest_V.magnitude
farthest = point if farther
farthest_V = origin_point_V if farther
end
angle = reference_V.angle(farthest_V)
invalid("#{id} polar angle", mth, 0, ERROR, 0) if angle.nil?
angle = 0 if angle.nil?
- adjust = false # adjust angle [180°, 360°] if necessary
+ adjust = false # adjust angle [180°, 360°] if necessary
if vertical
adjust = true if east.dot(farthest_V) < -TOL
else
- if north.dot(farthest_V).abs < TOL ||
- (north.dot(farthest_V).abs - 1).abs < TOL
+ dN = north.dot(farthest_V)
+ dN1 = north.dot(farthest_V).abs - 1
+
+ if dN.abs < TOL || dN1.abs < TOL
adjust = true if east.dot(farthest_V) < -TOL
else
- adjust = true if north.dot(farthest_V) < -TOL
+ adjust = true if dN < -TOL
end
end
angle = 2 * Math::PI - angle if adjust
angle -= 2 * Math::PI if (angle - 2 * Math::PI).abs < TOL
surface[:angle ] = angle
farthest_V.normalize!
surface[:polar ] = farthest_V
surface[:normal] = normal
- end # end of edge-linked, surface-to-wire loop
- end # end of edge-linked surface loop
+ end # end of edge-linked, surface-to-wire loop
+ end # end of edge-linked surface loop
edge[:horizontal] = horizontal
edge[:vertical ] = vertical
- edge[:surfaces ] = edge[:surfaces].sort_by{ |i, p| p[:angle] }.to_h
- end # end of edge loop
+ edge[:surfaces ] = edge[:surfaces].sort_by{ |_, p| p[:angle] }.to_h
+ end # end of edge loop
# Topolys edges may constitute thermal bridges (and therefore thermally
# derate linked OpenStudio opaque surfaces), depending on a number of
# factors such as surface type, space conditioning and boundary conditions.
# Thermal bridging attributes (type & PSI-value pairs) are grouped into PSI
@@ -1249,27 +1807,31 @@
# ... in such circumstances, TBD will halt all processes and exit while
# signaling to OpenStudio to halt its own processes (e.g., not launch an
# EnergyPlus simulation). This is similar to accessing an invalid .osm file.
return tbd if fatal?
- psi = json[:io][:building][:psi] # default building PSI on file
+ psi = json[:io][:building][:psi] # default building PSI on file
shorts = json[:psi].shorthands(psi)
- empty = shorts[:has].empty? || shorts[:val].empty?
- log(FTL, "Invalid or incomplete building PSI set (#{mth})") if empty
- return tbd if empty
+ if shorts[:has].empty? || shorts[:val].empty?
+ log(FTL, "Invalid or incomplete building PSI set (#{mth})")
+ return tbd
+ end
+
edges.values.each do |edge|
next unless edge.key?(:surfaces)
+
deratables = []
+ set = {}
edge[:surfaces].keys.each do |id|
next unless tbd[:surfaces].key?(id)
+
deratables << id if tbd[:surfaces][id][:deratable]
end
next if deratables.empty?
- set = {}
if edge.key?(:io_type)
bdg = json[:psi].safe(psi, edge[:io_type]) # building safe type fallback
edge[:sets] = {} unless edge.key?(:sets)
edge[:sets][edge[:io_type]] = shorts[:val][bdg] # building safe fallback
@@ -1288,110 +1850,239 @@
break if match
next unless tbd[:surfaces].key?(id)
next unless deratables.include?(id)
# Evaluate current set content before processing a new linked surface.
- is = {}
- is[:head ] = set.keys.to_s.include?("head" )
- is[:sill ] = set.keys.to_s.include?("sill" )
- is[:jamb ] = set.keys.to_s.include?("jamb" )
- is[:corner ] = set.keys.to_s.include?("corner" )
- is[:parapet ] = set.keys.to_s.include?("parapet" )
- is[:party ] = set.keys.to_s.include?("party" )
- is[:grade ] = set.keys.to_s.include?("grade" )
- is[:balcony ] = set.keys.to_s.include?("balcony" )
- is[:rimjoist] = set.keys.to_s.include?("rimjoist")
+ is = {}
+ is[:head ] = set.keys.to_s.include?("head")
+ is[:sill ] = set.keys.to_s.include?("sill")
+ is[:jamb ] = set.keys.to_s.include?("jamb")
+ is[:doorhead ] = set.keys.to_s.include?("doorhead")
+ is[:doorsill ] = set.keys.to_s.include?("doorsill")
+ is[:doorjamb ] = set.keys.to_s.include?("doorjamb")
+ is[:skylighthead ] = set.keys.to_s.include?("skylighthead")
+ is[:skylightsill ] = set.keys.to_s.include?("skylightsill")
+ is[:skylightjamb ] = set.keys.to_s.include?("skylightjamb")
+ is[:spandrel ] = set.keys.to_s.include?("spandrel")
+ is[:corner ] = set.keys.to_s.include?("corner")
+ is[:parapet ] = set.keys.to_s.include?("parapet")
+ is[:roof ] = set.keys.to_s.include?("roof")
+ is[:party ] = set.keys.to_s.include?("party")
+ is[:grade ] = set.keys.to_s.include?("grade")
+ is[:balcony ] = set.keys.to_s.include?("balcony")
+ is[:balconysill ] = set.keys.to_s.include?("balconysill")
+ is[:balconydoorsill ] = set.keys.to_s.include?("balconydoorsill")
+ is[:rimjoist ] = set.keys.to_s.include?("rimjoist")
- # Label edge as :head, :sill or :jamb if linked to:
- # 1x subsurface
+ # Label edge as ...
+ # :head, :sill, :jamb (vertical fenestration)
+ # :doorhead, :doorsill, :doorjamb (opaque door)
+ # :skylighthead, :skylightsill, :skylightjamb (all other cases)
+ #
+ # ... if linked to:
+ # 1x subsurface (vertical or non-vertical)
edge[:surfaces].keys.each do |i|
- break if is[:head] || is[:sill] || is[:jamb]
+ break if is[:head ]
+ break if is[:sill ]
+ break if is[:jamb ]
+ break if is[:doorhead ]
+ break if is[:doorsill ]
+ break if is[:doorjamb ]
+ break if is[:skylighthead]
+ break if is[:skylightsill]
+ break if is[:skylightjamb]
next if deratables.include?(i)
next unless holes.key?(i)
- gardian = ""
- gardian = id if deratables.size == 1 # just dad
+ # In most cases, subsurface edges simply delineate the rough opening
+ # of its base surface (here, a "gardian"). Door sills, corner windows,
+ # as well as a subsurface header aligned with a plenum "floor"
+ # (ceiling tiles), are common instances where a subsurface edge links
+ # 2x (opaque) surfaces. Deratable surface "id" may not be the gardian
+ # of subsurface "i" - the latter may be a neighbour. The single
+ # surface to derate is not the gardian in such cases.
+ gardian = deratables.size == 1 ? id : ""
+ target = gardian
- if gardian.empty? # seek uncle
- pops = {} # kids?
- uncles = {} # nieces?
- boys = [] # kids
- nieces = [] # nieces
- uncle = deratables.first unless deratables.first == id # uncle 1st?
- uncle = deratables.last unless deratables.last == id # uncle 2nd?
+ # Retrieve base surface's subsurfaces.
+ windows = tbd[:surfaces][id].key?(:windows)
+ doors = tbd[:surfaces][id].key?(:doors)
+ skylights = tbd[:surfaces][id].key?(:skylights)
- pops[:w ] = tbd[:surfaces][id ].key?(:windows )
- pops[:d ] = tbd[:surfaces][id ].key?(:doors )
- pops[:s ] = tbd[:surfaces][id ].key?(:skylights)
- uncles[:w] = tbd[:surfaces][uncle].key?(:windows )
- uncles[:d] = tbd[:surfaces][uncle].key?(:doors )
- uncles[:s] = tbd[:surfaces][uncle].key?(:skylights)
+ windows = windows ? tbd[:surfaces][id][:windows ] : {}
+ doors = doors ? tbd[:surfaces][id][:doors ] : {}
+ skylights = skylights ? tbd[:surfaces][id][:skylights] : {}
- boys += tbd[:surfaces][id ][:windows ].keys if pops[:w]
- boys += tbd[:surfaces][id ][:doors ].keys if pops[:d]
- boys += tbd[:surfaces][id ][:skylights].keys if pops[:s]
- nieces += tbd[:surfaces][uncle][:windows ].keys if uncles[:w]
- nieces += tbd[:surfaces][uncle][:doors ].keys if uncles[:d]
- nieces += tbd[:surfaces][uncle][:skylights].keys if uncles[:s]
+ # The gardian is "id" if subsurface "ids" holds "i".
+ ids = windows.keys + doors.keys + skylights.keys
- gardian = uncle if boys.include?(i)
- gardian = id if nieces.include?(i)
+ if gardian.empty?
+ other = deratables.first == id ? deratables.last : deratables.first
+
+ gardian = ids.include?(i) ? id : other
+ target = ids.include?(i) ? other : id
+
+ windows = tbd[:surfaces][gardian].key?(:windows)
+ doors = tbd[:surfaces][gardian].key?(:doors)
+ skylights = tbd[:surfaces][gardian].key?(:skylights)
+
+ windows = windows ? tbd[:surfaces][gardian][:windows ] : {}
+ doors = doors ? tbd[:surfaces][gardian][:doors ] : {}
+ skylights = skylights ? tbd[:surfaces][gardian][:skylights] : {}
+
+ ids = windows.keys + doors.keys + skylights.keys
end
- next if gardian.empty?
- s1 = edge[:surfaces][gardian]
- s2 = edge[:surfaces][i]
+ unless ids.include?(i)
+ log(ERR, "Orphaned subsurface #{i} (mth)")
+ next
+ 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?
+ sub = skylight unless skylight.empty?
+
+ window = sub[:type] == :window
+ door = sub[:type] == :door
+ glazed = door && sub.key?(:glazed) && sub[:glazed]
+
+ s1 = edge[:surfaces][target]
+ s2 = edge[:surfaces][i ]
concave = concave?(s1, s2)
convex = convex?(s1, s2)
flat = !concave && !convex
- # Subsurface edges are tagged as :head, :sill or :jamb, regardless
- # of building PSI set subsurface tags. If the latter is simply
- # :fenestration, then its (single) PSI value is systematically
- # attributed to subsurface :head, :sill & :jamb edges. If absent,
- # concave or convex variants also inherit from base type.
+ # Subsurface edges are tagged as head, sill or jamb, regardless of
+ # building PSI set subsurface-related tags. If the latter is simply
+ # :fenestration, then its single PSI factor is systematically
+ # assigned to e.g. a window's :head, :sill & :jamb edges.
#
- # TBD tags a subsurface edge as :jamb if the subsurface is "flat".
- # If not flat, TBD tags a horizontal edge as either :head or :sill
- # based on the polar angle of the subsurface around the edge vs sky
- # zenith. Otherwise, all other subsurface edges are tagged as :jamb.
- if ((s2[:normal].dot(zenith)).abs - 1).abs < TOL
- set[:jamb ] = shorts[:val][:jamb ] if flat
- set[:jambconcave] = shorts[:val][:jambconcave] if concave
- set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
- is[:jamb ] = true
- else
- if edge[:horizontal]
- if s2[:polar].dot(zenith) < 0
- set[:head ] = shorts[:val][:head ] if flat
- set[:headconcave] = shorts[:val][:headconcave] if concave
- set[:headconvex ] = shorts[:val][:headconvex ] if convex
- is[:head ] = true
- else
- set[:sill ] = shorts[:val][:sill ] if flat
- set[:sillconcave] = shorts[:val][:sillconcave] if concave
- set[:sillconvex ] = shorts[:val][:sillconvex ] if convex
- is[:sill ] = true
- end
- else
+ # Additionally, concave or convex variants also inherit from the base
+ # type if undefined in the PSI set.
+ #
+ # If a subsurface is not horizontal, TBD tags any horizontal edge as
+ # either :head or :sill based on the polar angle of the subsurface
+ # around the edge vs sky zenith. Otherwise, all other subsurface edges
+ # are tagged as :jamb.
+ if ((s2[:normal].dot(zenith)).abs - 1).abs < TOL # horizontal surface
+ if glazed || window
set[:jamb ] = shorts[:val][:jamb ] if flat
set[:jambconcave] = shorts[:val][:jambconcave] if concave
set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
is[:jamb ] = true
+ elsif door
+ set[:doorjamb ] = shorts[:val][:doorjamb ] if flat
+ set[:doorjambconcave] = shorts[:val][:doorjambconcave] if concave
+ set[:doorjambconvex ] = shorts[:val][:doorjambconvex ] if convex
+ is[:doorjamb ] = true
+ else
+ set[:skylightjamb ] = shorts[:val][:skylightjamb ] if flat
+ set[:skylightjambconcave] = shorts[:val][:skylightjambconcave] if concave
+ set[:skylightjambconvex ] = shorts[:val][:skylightjambconvex ] if convex
+ is[:skylightjamb ] = true
end
+ else
+ if glazed || window
+ if edge[:horizontal]
+ if s2[:polar].dot(zenith) < 0
+ set[:head ] = shorts[:val][:head ] if flat
+ set[:headconcave] = shorts[:val][:headconcave] if concave
+ set[:headconvex ] = shorts[:val][:headconvex ] if convex
+ is[:head ] = true
+ else
+ set[:sill ] = shorts[:val][:sill ] if flat
+ set[:sillconcave] = shorts[:val][:sillconcave] if concave
+ set[:sillconvex ] = shorts[:val][:sillconvex ] if convex
+ is[:sill ] = true
+ end
+ else
+ set[:jamb ] = shorts[:val][:jamb ] if flat
+ set[:jambconcave] = shorts[:val][:jambconcave] if concave
+ set[:jambconvex ] = shorts[:val][:jambconvex ] if convex
+ is[:jamb ] = true
+ end
+ elsif door
+ if edge[:horizontal]
+ if s2[:polar].dot(zenith) < 0
+
+ set[:doorhead ] = shorts[:val][:doorhead ] if flat
+ set[:doorheadconcave] = shorts[:val][:doorheadconcave] if concave
+ set[:doorheadconvex ] = shorts[:val][:doorheadconvex ] if convex
+ is[:doorhead ] = true
+ else
+ set[:doorsill ] = shorts[:val][:doorsill ] if flat
+ set[:doorsillconcave] = shorts[:val][:doorsillconcave] if concave
+ set[:doorsillconvex ] = shorts[:val][:doorsillconvex ] if convex
+ is[:doorsill ] = true
+ end
+ else
+ set[:doorjamb ] = shorts[:val][:doorjamb ] if flat
+ set[:doorjambconcave] = shorts[:val][:doorjambconcave] if concave
+ set[:doorjambconvex ] = shorts[:val][:doorjambconvex ] if convex
+ is[:doorjamb ] = true
+ end
+ else
+ if edge[:horizontal]
+ if s2[:polar].dot(zenith) < 0
+ set[:skylighthead ] = shorts[:val][:skylighthead ] if flat
+ set[:skylightheadconcave] = shorts[:val][:skylightheadconcave] if concave
+ set[:skylightheadconvex ] = shorts[:val][:skylightheadconvex ] if convex
+ is[:skylighthead ] = true
+ else
+ set[:skylightsill ] = shorts[:val][:skylightsill ] if flat
+ set[:skylightsillconcave] = shorts[:val][:skylightsillconcave] if concave
+ set[:skylightsillconvex ] = shorts[:val][:skylightsillconvex ] if convex
+ is[:skylightsill ] = true
+ end
+ else
+ set[:skylightjamb ] = shorts[:val][:skylightjamb ] if flat
+ set[:skylightjambconcave] = shorts[:val][:skylightjambconcave] if concave
+ set[:skylightjambconvex ] = shorts[:val][:skylightjambconvex ] if convex
+ is[:skylightjamb ] = true
+ end
+ end
end
end
+ # Label edge as :spandrel if linked to:
+ # 1x deratable, non-spandrel wall
+ # 1x deratable, spandrel wall
+ edge[:surfaces].keys.each do |i|
+ break if is[:spandrel]
+ break unless deratables.size == 2
+ break unless walls.key?(id)
+ break unless walls[id][:spandrel]
+ next if i == id
+ next unless deratables.include?(i)
+ next unless walls.key?(i)
+ next if walls[i][:spandrel]
+
+ s1 = edge[:surfaces][id]
+ s2 = edge[:surfaces][i ]
+ concave = concave?(s1, s2)
+ convex = convex?(s1, s2)
+ flat = !concave && !convex
+
+ set[:spandrel ] = shorts[:val][:spandrel ] if flat
+ set[:spandrelconcave] = shorts[:val][:spandrelconcave] if concave
+ set[:spandrelconvex ] = shorts[:val][:spandrelconvex ] if convex
+ is[:spandrel ] = true
+ end
+
# Label edge as :cornerconcave or :cornerconvex if linked to:
# 2x deratable walls & f(relative polar wall vectors around edge)
edge[:surfaces].keys.each do |i|
break if is[:corner]
break unless deratables.size == 2
break unless walls.key?(id)
next if i == id
- next unless deratables.include?(i)
- next unless walls.key?(i)
+ next unless deratables.include?(i)
+ next unless walls.key?(i)
s1 = edge[:surfaces][id]
s2 = edge[:surfaces][i]
concave = concave?(s1, s2)
convex = convex?(s1, s2)
@@ -1399,31 +2090,39 @@
set[:cornerconcave] = shorts[:val][:cornerconcave] if concave
set[:cornerconvex ] = shorts[:val][:cornerconvex ] if convex
is[:corner ] = true
end
- # Label edge as :parapet if linked to:
+ # Label edge as :parapet/:roof if linked to:
# 1x deratable wall
# 1x deratable ceiling
edge[:surfaces].keys.each do |i|
break if is[:parapet]
+ break if is[:roof ]
break unless deratables.size == 2
break unless ceilings.key?(id)
next if i == id
next unless deratables.include?(i)
next unless walls.key?(i)
s1 = edge[:surfaces][id]
- s2 = edge[:surfaces][i]
+ s2 = edge[:surfaces][i ]
concave = concave?(s1, s2)
convex = convex?(s1, s2)
flat = !concave && !convex
- set[:parapet ] = shorts[:val][:parapet ] if flat
- set[:parapetconcave] = shorts[:val][:parapetconcave] if concave
- set[:parapetconvex ] = shorts[:val][:parapetconvex ] if convex
- is[:parapet ] = true
+ if argh[:parapet]
+ set[:parapet ] = shorts[:val][:parapet ] if flat
+ set[:parapetconcave] = shorts[:val][:parapetconcave] if concave
+ set[:parapetconvex ] = shorts[:val][:parapetconvex ] if convex
+ is[:parapet ] = true
+ else
+ set[:roof ] = shorts[:val][:roof ] if flat
+ set[:roofconcave] = shorts[:val][:roofconcave] if concave
+ set[:roofconvex ] = shorts[:val][:roofconvex ] if convex
+ is[:roof ] = true
+ end
end
# Label edge as :party if linked to:
# 1x OtherSideCoefficients surface
# 1x (only) deratable surface
@@ -1437,11 +2136,11 @@
facing = tbd[:surfaces][i][:boundary].downcase
next unless facing == "othersidecoefficients"
s1 = edge[:surfaces][id]
- s2 = edge[:surfaces][i]
+ s2 = edge[:surfaces][i ]
concave = concave?(s1, s2)
convex = convex?(s1, s2)
flat = !concave && !convex
set[:party ] = shorts[:val][:party ] if flat
@@ -1471,73 +2170,161 @@
set[:gradeconcave] = shorts[:val][:gradeconcave] if concave
set[:gradeconvex ] = shorts[:val][:gradeconvex ] if convex
is[:grade ] = true
end
- # Label edge as :rimjoist (or :balcony) if linked to:
+ # Label edge as :rimjoist, :balcony, :balconysill or :balconydoorsill if linked to:
# 1x deratable surface
# 1x CONDITIONED floor
# 1x shade (optional)
- balcony = false
+ # 1x subsurface (optional)
+ #
+ # Despite referring to 'sill' or 'doorsill', a 'balconysill' or
+ # 'balconydoorsill' edge may instead link (rarer) cases of balcony and a
+ # fenestratio/door head. ASHRAE 90.1 2022 does not make the distinction
+ # between sill vs head when intermediatre floor, balcony and vertical
+ # fenestration meet. 'Sills' are simply the most common occurrence.
+ balcony = false
+ balconysill = false # vertical fenestration
+ balconydoorsill = false # opaque door
edge[:surfaces].keys.each do |i|
- break if balcony
- next if i == id
- balcony = true if shades.key?(i)
+ break if balcony
+ next if i == id
+
+ balcony = shades.key?(i)
end
edge[:surfaces].keys.each do |i|
- break if is[:rimjoist] || is[:balcony]
+ break unless balcony
+ break if balconysill
+ break if balconydoorsill
+ next if i == id
+ next unless holes.key?(i)
+
+ # Deratable surface "id" may not be the gardian of "i" (see sills).
+ gardian = deratables.size == 1 ? id : ""
+ target = gardian
+
+ # Retrieve base surface's subsurfaces.
+ windows = tbd[:surfaces][id].key?(:windows)
+ doors = tbd[:surfaces][id].key?(:doors)
+ skylights = tbd[:surfaces][id].key?(:skylights)
+
+ windows = windows ? tbd[:surfaces][id][:windows ] : {}
+ doors = doors ? tbd[:surfaces][id][:doors ] : {}
+ skylights = skylights ? tbd[:surfaces][id][:skylights] : {}
+
+ # The gardian is "id" if subsurface "ids" holds "i".
+ ids = windows.keys + doors.keys + skylights.keys
+
+ if gardian.empty?
+ other = deratables.first == id ? deratables.last : deratables.first
+
+ gardian = ids.include?(i) ? id : other
+ target = ids.include?(i) ? other : id
+
+ windows = tbd[:surfaces][gardian].key?(:windows)
+ doors = tbd[:surfaces][gardian].key?(:doors)
+ skylights = tbd[:surfaces][gardian].key?(:skylights)
+
+ windows = windows ? tbd[:surfaces][gardian][:windows ] : {}
+ doors = doors ? tbd[:surfaces][gardian][:doors ] : {}
+ skylights = skylights ? tbd[:surfaces][gardian][:skylights] : {}
+
+ ids = windows.keys + doors.keys + skylights.keys
+ end
+
+ unless ids.include?(i)
+ log(ERR, "Balcony sill: orphaned subsurface #{i} (mth)")
+ next
+ 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?
+ sub = skylight unless skylight.empty?
+
+ window = sub[:type] == :window
+ door = sub[:type] == :door
+ glazed = door && sub.key?(:glazed) && sub[:glazed]
+
+ if window || glazed
+ balconysill = true
+ elsif door
+ balconydoorsill = true
+ end
+ end
+
+ edge[:surfaces].keys.each do |i|
+ break if is[:rimjoist ] || is[:balcony ] ||
+ is[:balconysill] || is[:balconydoorsill]
break unless deratables.size > 0
break if floors.key?(id)
next if i == id
next unless floors.key?(i)
next unless floors[i].key?(:conditioned)
next unless floors[i][:conditioned]
- next if floors[i][:ground]
+ next if floors[i][:ground ]
other = deratables.first unless deratables.first == id
other = deratables.last unless deratables.last == id
+ other = id if deratables.size == 1
s1 = edge[:surfaces][id]
s2 = edge[:surfaces][other]
concave = concave?(s1, s2)
convex = convex?(s1, s2)
flat = !concave && !convex
- if balcony
- set[:balcony ] = shorts[:val][:balcony ] if flat
- set[:balconyconcave ] = shorts[:val][:balconyconcave ] if concave
- set[:balconyconvex ] = shorts[:val][:balconyconvex ] if convex
- is[:balcony ] = true
+ if balconydoorsill
+ set[:balconydoorsill ] = shorts[:val][:balconydoorsill ] if flat
+ set[:balconydoorsillconcave] = shorts[:val][:balconydoorsillconcave] if concave
+ set[:balconydoorsillconvex ] = shorts[:val][:balconydoorsillconvex ] if convex
+ is[:balconydoorsill ] = true
+ elsif balconysill
+ set[:balconysill ] = shorts[:val][:balconysill ] if flat
+ set[:balconysillconcave ] = shorts[:val][:balconysillconcave ] if concave
+ set[:balconysillconvex ] = shorts[:val][:balconysillconvex ] if convex
+ is[:balconysill ] = true
+ elsif balcony
+ set[:balcony ] = shorts[:val][:balcony ] if flat
+ set[:balconyconcave ] = shorts[:val][:balconyconcave ] if concave
+ set[:balconyconvex ] = shorts[:val][:balconyconvex ] if convex
+ is[:balcony ] = true
else
- set[:rimjoist ] = shorts[:val][:rimjoist ] if flat
- set[:rimjoistconcave] = shorts[:val][:rimjoistconcave] if concave
- set[:rimjoistconvex ] = shorts[:val][:rimjoistconvex ] if convex
- is[:rimjoist ] = true
+ set[:rimjoist ] = shorts[:val][:rimjoist ] if flat
+ set[:rimjoistconcave ] = shorts[:val][:rimjoistconcave ] if concave
+ set[:rimjoistconvex ] = shorts[:val][:rimjoistconvex ] if convex
+ is[:rimjoist ] = true
end
end
- end # edge's surfaces loop
+ end # edge's surfaces loop
edge[:psi] = set unless set.empty?
edge[:set] = psi unless set.empty?
- end # edge loop
+ end # edge loop
# Tracking (mild) transitions between deratable surfaces around edges that
# have not been previously tagged.
edges.values.each do |edge|
+ deratable = false
next if edge.key?(:psi)
next unless edge.key?(:surfaces)
- deratable = false
edge[:surfaces].keys.each do |id|
next unless tbd[:surfaces].key?(id)
next unless tbd[:surfaces][id][:deratable]
+
deratable = tbd[:surfaces][id][:deratable]
end
next unless deratable
+
edge[:psi] = { transition: 0.000 }
edge[:set] = json[:io][:building][:psi]
end
# 'Unhinged' subsurfaces, like Tubular Daylight Device (TDD) domes,
@@ -1545,20 +2332,23 @@
# parent roof surface. Add parent surface ID to unhinged edges.
edges.values.each do |edge|
next if edge.key?(:psi)
next unless edge.key?(:surfaces)
next unless edge[:surfaces].size == 1
+
id = edge[:surfaces].first.first
next unless holes.key?(id)
next unless holes[id].attributes.key?(:unhinged)
next unless holes[id].attributes[:unhinged]
subsurface = model.getSubSurfaceByName(id)
next if subsurface.empty?
+
subsurface = subsurface.get
surface = subsurface.surface
next if surface.empty?
+
nom = surface.get.nameString
next unless tbd[:surfaces].key?(nom)
next unless tbd[:surfaces][nom].key?(:conditioned)
next unless tbd[:surfaces][nom][:conditioned]
@@ -1568,147 +2358,258 @@
set[:jamb] = shorts[:val][:jamb]
edge[:psi] = set
edge[:set] = json[:io][:building][:psi]
end
- # A priori, TBD applies (default) :building PSI types and values to
- # individual edges. If a TBD JSON input file holds custom PSI sets for:
- # :stories
- # :spacetypes
- # :surfaces
- # :edges
- # ... that may apply to individual edges, then the default :building PSI
- # types and/or values are overridden, as follows:
- # custom :stories PSI sets trump :building PSI sets
- # custom :spacetypes PSI sets trump aforementioned PSI sets
- # custom :spaces PSI sets trump aforementioned PSI sets
- # custom :surfaces PSI sets trump aforementioned PSI sets
- # custom :edges PSI sets trump aforementioned PSI sets
if json[:io]
- if json[:io].key?(:subsurfaces) # reset subsurface U-factors (if on file)
+ # Reset subsurface U-factors (if on file).
+ if json[:io].key?(:subsurfaces)
json[:io][:subsurfaces].each do |sub|
+ match = false
next unless sub.key?(:id)
next unless sub.key?(:usi)
- match = false
tbd[:surfaces].values.each do |surface|
break if match
[:windows, :doors, :skylights].each do |types|
- if surface.key?(types)
- surface[types].each do |id, opening|
- break if match
- next unless opening.key?(:u)
- match = true if sub[:id] == id
- opening[:u] = sub[:usi] if sub[:id] == id
- end
+ break if match
+ next unless surface.key?(types)
+
+ surface[types].each do |id, opening|
+ break if match
+ next unless opening.key?(:u)
+ next unless sub[:id] == id
+
+ opening[:u] = sub[:usi]
+ match = true
end
end
end
end
end
+ # Reset wall-to-roof intersection type (if on file) ... per group.
[:stories, :spacetypes, :spaces].each do |groups|
key = :story
key = :stype if groups == :spacetypes
key = :space if groups == :spaces
- next unless json[:io].key?(groups)
+ next unless json[:io].key?(groups)
json[:io][groups].each do |group|
next unless group.key?(:id)
+ next unless group.key?(:parapet)
+
+ edges.values.each do |edge|
+ match = false
+ next unless edge.key?(:psi)
+ next unless edge.key?(:surfaces)
+ next if edge.key?(:io_type)
+
+ edge[:surfaces].keys.each do |id|
+ break if match
+ next unless tbd[:surfaces].key?(id)
+ next unless tbd[:surfaces][id].key?(key)
+
+ match = group[:id] == tbd[:surfaces][id][key].nameString
+ end
+
+ next unless match
+
+ parapets = edge[:psi].keys.select {|ty| ty.to_s.include?("parapet")}
+ roofs = edge[:psi].keys.select {|ty| ty.to_s.include?("roof")}
+
+ if group[:parapet]
+ next unless parapets.empty?
+ next if roofs.empty?
+
+ type = :parapet
+ type = :parapetconcave if roofs.first.to_s.include?("concave")
+ type = :parapetconvex if roofs.first.to_s.include?("convex")
+
+ edge[:psi][type] = shorts[:val][type]
+ roofs.each {|ty| edge[:psi].delete(ty)}
+ else
+ next unless roofs.empty?
+ next if parapets.empty?
+
+ type = :roof
+ type = :roofconcave if parapets.first.to_s.include?("concave")
+ type = :roofconvex if parapets.first.to_s.include?("convex")
+
+ edge[:psi][type] = shorts[:val][type]
+
+ parapets.each { |ty| edge[:psi].delete(ty) }
+ end
+ end
+ end
+ end
+
+ # Reset wall-to-roof intersection type (if on file) - individual surfaces.
+ if json[:io].key?(:surfaces)
+ json[:io][:surfaces].each do |surface|
+ next unless surface.key?(:parapet)
+ next unless surface.key?(:id)
+
+ edges.values.each do |edge|
+ next if edge.key?(:io_type)
+ next unless edge.key?(:psi)
+ next unless edge.key?(:surfaces)
+ next unless edge[:surfaces].keys.include?(surface[:id])
+
+ parapets = edge[:psi].keys.select {|ty| ty.to_s.include?("parapet")}
+ roofs = edge[:psi].keys.select {|ty| ty.to_s.include?("roof")}
+
+
+ if surface[:parapet]
+ next unless parapets.empty?
+ next if roofs.empty?
+
+ type = :parapet
+ type = :parapetconcave if roofs.first.to_s.include?("concave")
+ type = :parapetconvex if roofs.first.to_s.include?("convex")
+
+ edge[:psi][type] = shorts[:val][type]
+ roofs.each {|ty| edge[:psi].delete(ty)}
+ else
+ next unless roofs.empty?
+ next if parapets.empty?
+
+ type = :roof
+ type = :roofconcave if parapets.first.to_s.include?("concave")
+ type = :roofconvex if parapets.first.to_s.include?("convex")
+
+ edge[:psi][type] = shorts[:val][type]
+ parapets.each {|ty| edge[:psi].delete(ty)}
+ end
+ end
+ end
+ end
+
+ # A priori, TBD applies (default) :building PSI types and values to
+ # individual edges. If a TBD JSON input file holds custom PSI sets for:
+ # :stories
+ # :spacetypes
+ # :surfaces
+ # :edges
+ # ... that may apply to individual edges, then the default :building PSI
+ # types and/or values are overridden, as follows:
+ # custom :stories PSI sets trump :building PSI sets
+ # custom :spacetypes PSI sets trump aforementioned PSI sets
+ # custom :spaces PSI sets trump aforementioned PSI sets
+ # custom :surfaces PSI sets trump aforementioned PSI sets
+ # custom :edges PSI sets trump aforementioned PSI sets
+ [:stories, :spacetypes, :spaces].each do |groups|
+ key = :story
+ key = :stype if groups == :spacetypes
+ key = :space if groups == :spaces
+ next unless json[:io].key?(groups)
+
+ json[:io][groups].each do |group|
+ next unless group.key?(:id)
next unless group.key?(:psi)
next unless json[:psi].set.key?(group[:psi])
- sh = json[:psi].shorthands(group[:psi])
- next if sh[:val].empty?
+ sh = json[:psi].shorthands(group[:psi])
+ next if sh[:val].empty?
+
edges.values.each do |edge|
- next if edge.key?(:io_set)
+ match = false
next unless edge.key?(:psi)
next unless edge.key?(:surfaces)
+ next if edge.key?(:io_set)
edge[:surfaces].keys.each do |id|
+ break if match
next unless tbd[:surfaces].key?(id)
next unless tbd[:surfaces][id].key?(key)
- next unless group[:id] == tbd[:surfaces][id][key].nameString
- edge[groups] = {} unless edge.key?(groups)
- edge[groups][group[:psi]] = {}
- set = {}
+ match = group[:id] == tbd[:surfaces][id][key].nameString
+ end
- if edge.key?(:io_type)
- safer = json[:psi].safe(group[:psi], edge[:io_type])
- set[edge[:io_type]] = sh[:val][safer] if safer
- else
- edge[:psi].keys.each do |type|
- safer = json[:psi].safe(group[:psi], type)
- set[type] = sh[:val][safer] if safer
- end
- end
+ next unless match
- edge[groups][group[:psi]] = set unless set.empty?
+ set = {}
+ edge[groups] = {} unless edge.key?(groups)
+ edge[groups][group[:psi]] = {}
+
+ if edge.key?(:io_type)
+ safer = json[:psi].safe(group[:psi], edge[:io_type])
+ set[edge[:io_type]] = sh[:val][safer] if safer
+ else
+ edge[:psi].keys.each do |type|
+ safer = json[:psi].safe(group[:psi], type)
+ set[type] = sh[:val][safer] if safer
+ end
end
+
+ edge[groups][group[:psi]] = set unless set.empty?
end
end
# TBD/Topolys edges will generally be linked to more than one surface
- # and hence to more than one story. It is possible for a TBD JSON file
- # to hold 2x story PSI sets that end up targetting one or more edges
- # common to both stories. In such cases, TBD retains the most conductive
- # PSI type/value from either story PSI set.
+ # and hence to more than one group. It is possible for a TBD JSON file
+ # to hold 2x group PSI sets that end up targetting one or more edges
+ # common to both groups. In such cases, TBD retains the most conductive
+ # PSI type/value from either group PSI set.
edges.values.each do |edge|
next unless edge.key?(:psi)
next unless edge.key?(groups)
edge[:psi].keys.each do |type|
vals = {}
edge[groups].keys.each do |set|
- sh = json[:psi].shorthands(set)
- next if sh[:val].empty?
+ sh = json[:psi].shorthands(set)
+ next if sh[:val].empty?
+
safer = json[:psi].safe(set, type)
vals[set] = sh[:val][safer] if safer
end
- next if vals.empty?
+ next if vals.empty?
+
edge[:psi ][type] = vals.values.max
edge[:sets] = {} unless edge.key?(:sets)
edge[:sets][type] = vals.key(vals.values.max)
end
end
end
if json[:io].key?(:surfaces)
json[:io][:surfaces].each do |surface|
- next unless surface.key?(:id)
next unless surface.key?(:psi)
+ next unless surface.key?(:id)
+ next unless tbd[:surfaces].key?(surface[:id ])
next unless json[:psi].set.key?(surface[:psi])
- sh = json[:psi].shorthands(surface[:psi])
- next if sh[:val].empty?
+ sh = json[:psi].shorthands(surface[:psi])
+ next if sh[:val].empty?
+
edges.values.each do |edge|
next if edge.key?(:io_set)
next unless edge.key?(:psi)
next unless edge.key?(:surfaces)
+ next unless edge[:surfaces].keys.include?(surface[:id])
- edge[:surfaces].each do |id, s|
- next unless tbd[:surfaces].key?(id)
- next unless surface[:id] == id
- set = {}
+ s = edge[:surfaces][surface[:id]]
+ set = {}
- if edge.key?(:io_type)
- safer = json[:psi].safe(surface[:psi], edge[:io_type])
- set[:io_type] = sh[:val][safer] if safer
- else
- edge[:psi].keys.each do |type|
- safer = json[:psi].safe(surface[:psi], type)
- set[type] = sh[:val][safer] if safer
- end
+ if edge.key?(:io_type)
+ safer = json[:psi].safe(surface[:psi], edge[:io_type])
+ set[:io_type] = sh[:val][safer] if safer
+ else
+ edge[:psi].keys.each do |type|
+ safer = json[:psi].safe(surface[:psi], type)
+ set[type] = sh[:val][safer] if safer
end
-
- s[:psi] = set unless set.empty?
- s[:set] = surface[:psi] unless set.empty?
end
+
+ next if set.empty?
+
+ s[:psi] = set
+ s[:set] = surface[:psi]
end
end
# TBD/Topolys edges will generally be linked to more than one surface. A
# TBD JSON file may hold 2x surface PSI sets that target a shared edge.
@@ -1719,21 +2620,24 @@
edge[:psi].keys.each do |type|
vals = {}
edge[:surfaces].each do |id, s|
- next unless s.key?(:psi)
- next unless s.key?(:set)
- next if s[:set].empty?
- sh = json[:psi].shorthands(s[:set])
- next if sh[:val].empty?
+ next unless s.key?(:psi)
+ next unless s.key?(:set)
+ next if s[:set].empty?
+
+ sh = json[:psi].shorthands(s[:set])
+ next if sh[:val].empty?
+
safer = json[:psi].safe(s[:set], type)
vals[s[:set]] = sh[:val][safer] if safer
end
- next if vals.empty?
- edge[:psi][type] = vals.values.max
+ next if vals.empty?
+
+ edge[:psi ][type] = vals.values.max
edge[:sets] = {} unless edge.key?(:sets)
edge[:sets][type] = vals.key(vals.values.max)
end
end
end
@@ -1744,19 +2648,22 @@
next unless edge.key?(:io_type)
next unless edge.key?(:surfaces)
if edge.key?(:io_set)
next unless json[:psi].set.key?(edge[:io_set])
+
set = edge[:io_set]
else
next unless edge[:sets].key?(edge[:io_type])
next unless json[:psi].set.key?(edge[:sets][edge[:io_type]])
+
set = edge[:sets][edge[:io_type]]
end
sh = json[:psi].shorthands(set)
next if sh[:val].empty?
+
safer = json[:psi].safe(set, edge[:io_type])
next unless safer
if edge.key?(:io_set)
edge[:psi] = {}
@@ -1770,13 +2677,14 @@
end
end
# Fetch edge multipliers for subsurfaces, if applicable.
edges.values.each do |edge|
- next if edge.key?(:mult) # skip if already assigned
+ next if edge.key?(:mult) # skip if already assigned
next unless edge.key?(:surfaces)
next unless edge.key?(:psi)
+
ok = false
edge[:psi].keys.each do |k|
break if ok
@@ -1784,14 +2692,14 @@
sill = k.to_s.include?("sill")
head = k.to_s.include?("head")
ok = jamb || sill || head
end
- next unless ok # if OK, edge links subsurface(s) ... yet which one(s)?
+ next unless ok # if OK, edge links subsurface(s) ... yet which one(s)?
edge[:surfaces].each do |id, surface|
- next unless tbd[:surfaces].key?(id) # look up parent (opaque) surface
+ next unless tbd[:surfaces].key?(id) # look up parent (opaque) surface
[:windows, :doors, :skylights].each do |subtypes|
next unless tbd[:surfaces][id].key?(subtypes)
tbd[:surfaces][id][subtypes].each do |nom, sub|
@@ -1799,11 +2707,11 @@
next unless sub[:mult] > 1
# An edge may be tagged with (potentially conflicting) multipliers.
# This is only possible if the edge links 2 subsurfaces, e.g. a
# shared jamb between window & door. By default, TBD tags common
- # subsurface edges as (mild) "transitions" (i.e. PSI 0 W/K.m), so
+ # subsurface edges as (mild) "transitions" (i.e. PSI 0 W/K•m), so
# there would be no point in assigning an edge multiplier. Users
# can however reset an edge type via a TBD JSON input file (e.g.
# "joint" instead of "transition"). It would be a very odd choice,
# but TBD doesn't prohibit it. If linked subsurfaces have different
# multipliers (e.g. 2 vs 3), TBD tracks the highest value.
@@ -1818,13 +2726,13 @@
# JSON input, reset any subsurface's head, sill or jamb edges as (mild)
# transitions when in close proximity to another subsurface edge. Both
# edges' origin and terminal vertices must be in close proximity. Edges
# of unhinged subsurfaces are ignored.
edges.each do |id, edge|
- nb = 0 # linked subsurfaces (i.e. "holes")
+ nb = 0 # linked subsurfaces (i.e. "holes")
match = false
- next if edge.key?(:io_type) # skip if set in JSON
+ next if edge.key?(:io_type) # skip if set in JSON
next unless edge.key?(:v0)
next unless edge.key?(:v1)
next unless edge.key?(:psi)
next unless edge.key?(:surfaces)
@@ -1877,10 +2785,11 @@
end
# Loop through each edge and assign heat loss to linked surfaces.
edges.each do |identifier, edge|
next unless edge.key?(:psi)
+
rsi = 0
max = edge[:psi ].values.max
type = edge[:psi ].key(max)
length = edge[:length]
length *= edge[:mult ] if edge.key?(:mult)
@@ -1894,15 +2803,16 @@
# Retrieve valid linked surfaces as deratables.
edge[:surfaces].each do |id, s|
next unless tbd[:surfaces].key?(id)
next unless tbd[:surfaces][id][:deratable]
+
deratables[id] = s
end
edge[:surfaces].each { |id, s| apertures[id] = s if holes.key?(id) }
- next if apertures.size > 1 # edge links 2x openings
+ next if apertures.size > 1 # edge links 2x openings
# Prune dad if edge links an opening, its dad and an uncle.
if deratables.size > 1 && apertures.size > 0
deratables.each do |id, deratable|
[:windows, :doors, :skylights].each do |types|
@@ -1918,10 +2828,11 @@
next if deratables.empty?
# Sum RSI of targeted insulating layer from each deratable surface.
deratables.each do |id, deratable|
next unless tbd[:surfaces][id].key?(:r)
+
rsi += tbd[:surfaces][id][:r]
end
# Assign heat loss from thermal bridges to surfaces, in proportion to
# insulating layer thermal resistance.
@@ -1936,12 +2847,14 @@
end
# Assign thermal bridging heat loss [in W/K] to each deratable surface.
tbd[:surfaces].each do |id, surface|
next unless surface.key?(:edges)
+
surface[:heatloss] = 0
e = surface[:edges].values
+
e.each { |edge| surface[:heatloss] += edge[:psi] * edge[:length] }
end
# Add point conductances (W/K x count), in TBD JSON file (under surfaces).
tbd[:surfaces].each do |id, s|
@@ -1957,23 +2870,25 @@
surface[:khis].each do |k|
next unless k.key?(:id)
next unless k.key?(:count)
next unless json[:khi].point.key?(k[:id])
next unless json[:khi].point[k[:id]] > 0.001
- s[:heatloss] = 0 unless s.key?(:heatloss)
- s[:heatloss] += json[:khi].point[k[:id]] * k[:count]
- s[:pts] = {} unless s.key?(:pts)
+
+ s[:heatloss] = 0 unless s.key?(:heatloss)
+ s[:heatloss] += json[:khi].point[k[:id]] * k[:count]
+ s[:pts ] = {} unless s.key?(:pts)
+
s[:pts][k[:id]] = { val: json[:khi].point[k[:id]], n: k[:count] }
end
end
end
# If user has selected a Ut to meet, e.g. argh'ments:
# :uprate_walls
# :wall_ut
- # :wall_option
- # (same triple arguments for roofs and exposed floors)
+ # :wall_option ... (same triple arguments for roofs and exposed floors)
+ #
# ... first 'uprate' targeted insulation layers (see ua.rb) before derating.
# Check for new argh keys [:wall_uo], [:roof_uo] and/or [:floor_uo].
up = argh[:uprate_walls] || argh[:uprate_roofs] || argh[:uprate_floors]
uprate(model, tbd[:surfaces], argh) if up
@@ -1983,89 +2898,93 @@
# 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.
tbd[:surfaces].each do |id, surface|
next unless surface.key?(:construction)
- next unless surface.key?(:index )
- next unless surface.key?(:ltype )
- next unless surface.key?(:r )
- next unless surface.key?(:edges )
- next unless surface.key?(:heatloss )
+ next unless surface.key?(:index)
+ next unless surface.key?(:ltype)
+ next unless surface.key?(:r)
+ next unless surface.key?(:edges)
+ next unless surface.key?(:heatloss)
next unless surface[:heatloss].abs > TOL
- model.getSurfaces.each do |s|
- next unless id == s.nameString
- index = surface[:index ]
- current_c = surface[:construction]
- c = current_c.clone(model).to_LayeredConstruction.get
- m = nil
- m = derate(model, id, surface, c) if index
- # m may be nilled simply because the targeted construction has already
- # been derated, i.e. holds " tbd" in its name. Names of cloned/derated
- # constructions (due to TBD) include the surface name (since derated
- # constructions are now unique to each surface) and the suffix " c tbd".
- if m
- c.setLayer(index, m)
- c.setName("#{id} c tbd")
- current_R = rsi(current_c, s.filmResistance)
+ s = model.getSurfaceByName(id)
+ next if s.empty?
- # In principle, the derated "ratio" could be calculated simply by
- # accessing a surface's uFactor. Yet air layers within constructions
- # (not air films) are ignored in OpenStudio's uFactor calculation.
- # An example would be 25mm-50mm pressure-equalized air gaps behind
- # brick veneer. This is not always compliant to some energy codes.
- # TBD currently factors-in air gap (and exterior cladding) R-values.
- #
- # If one comments out the following loop (3 lines), tested surfaces
- # with air layers will generate discrepencies between the calculed RSi
- # value above and the inverse of the uFactor. All other surface
- # constructions pass the test.
- #
- # if ((1/current_R) - s.uFactor.to_f).abs > 0.005
- # puts "#{s.nameString} - Usi:#{1/current_R} UFactor: #{s.uFactor}"
- # end
- s.setConstruction(c)
+ s = s.get
- # If the derated surface construction separates CONDITIONED space from
- # UNCONDITIONED or UNENCLOSED space, then derate the adjacent surface
- # construction as well (unless defaulted).
- if s.outsideBoundaryCondition.downcase == "surface"
- unless s.adjacentSurface.empty?
- adjacent = s.adjacentSurface.get
- nom = adjacent.nameString
- default = adjacent.isConstructionDefaulted == false
+ index = surface[:index ]
+ current_c = surface[:construction]
+ c = current_c.clone(model).to_LayeredConstruction.get
+ m = nil
+ m = derate(id, surface, c) if index
+ # m may be nilled simply because the targeted construction has already
+ # been derated, i.e. holds " tbd" in its name. Names of cloned/derated
+ # constructions (due to TBD) include the surface name (since derated
+ # constructions are now unique to each surface) and the suffix " c tbd".
+ if m
+ c.setLayer(index, m)
+ c.setName("#{id} c tbd")
+ current_R = rsi(current_c, s.filmResistance)
- if default && tbd[:surfaces].key?(nom)
- current_cc = tbd[:surfaces][nom][:construction]
- cc = current_cc.clone(model).to_LayeredConstruction.get
+ # In principle, the derated "ratio" could be calculated simply by
+ # accessing a surface's uFactor. Yet air layers within constructions
+ # (not air films) are ignored in OpenStudio's uFactor calculation.
+ # An example would be 25mm-50mm pressure-equalized air gaps behind
+ # brick veneer. This is not always compliant to some energy codes.
+ # TBD currently factors-in air gap (and exterior cladding) R-values.
+ #
+ # If one comments out the following loop (3 lines), tested surfaces
+ # with air layers will generate discrepencies between the calculed RSi
+ # value above and the inverse of the uFactor. All other surface
+ # constructions pass the test.
+ #
+ # if ((1/current_R) - s.uFactor.to_f).abs > 0.005
+ # puts "#{s.nameString} - Usi:#{1/current_R} UFactor: #{s.uFactor}"
+ # end
+ s.setConstruction(c)
- cc.setLayer(tbd[:surfaces][nom][:index], m)
- cc.setName("#{nom} c tbd")
- adjacent.setConstruction(cc)
- end
+ # If the derated surface construction separates CONDITIONED space from
+ # UNCONDITIONED or UNENCLOSED space, then derate the adjacent surface
+ # construction as well (unless defaulted).
+ if s.outsideBoundaryCondition.downcase == "surface"
+ unless s.adjacentSurface.empty?
+ adjacent = s.adjacentSurface.get
+ nom = adjacent.nameString
+ default = adjacent.isConstructionDefaulted == false
+
+ if default && tbd[:surfaces].key?(nom)
+ current_cc = tbd[:surfaces][nom][:construction]
+ cc = current_cc.clone(model).to_LayeredConstruction.get
+ cc.setLayer(tbd[:surfaces][nom][:index], m)
+ cc.setName("#{nom} c tbd")
+ adjacent.setConstruction(cc)
end
end
+ end
- # Compute updated RSi value from layers.
- updated_c = s.construction.get.to_LayeredConstruction.get
- updated_R = rsi(updated_c, s.filmResistance)
- ratio = -(current_R - updated_R) * 100 / current_R
+ # Compute updated RSi value from layers.
+ updated_c = s.construction.get.to_LayeredConstruction.get
+ updated_R = rsi(updated_c, s.filmResistance)
+ ratio = -(current_R - updated_R) * 100 / current_R
- surface[:ratio] = ratio if ratio.abs > TOL
- surface[:u ] = 1 / current_R # un-derated U-factors (for UA')
- end
+ surface[:ratio] = ratio if ratio.abs > TOL
+ surface[:u ] = 1 / current_R # un-derated U-factors (for UA')
end
end
# Ensure deratable surfaces have U-factors (even if NOT derated).
tbd[:surfaces].each do |id, surface|
next unless surface[:deratable]
next unless surface.key?(:construction)
next if surface.key?(:u)
- s = model.getSurfaceByName(id)
- log(ERR, "Skipping missing surface '#{id}' (#{mth})") if s.empty?
- next if s.empty?
+
+ s = model.getSurfaceByName(id)
+ msg = "Skipping missing surface '#{id}' (#{mth})"
+ log(ERR, msg) if s.empty?
+ next if s.empty?
+
surface[:u] = 1.0 / rsi(surface[:construction], s.get.filmResistance)
end
json[:io][:edges] = []
# Enrich io with TBD/Topolys edge info before returning:
@@ -2073,64 +2992,95 @@
# 2. edge PSI type
# 3. edge length (m)
# 4. edge origin & end vertices
# 5. array of linked outside- or ground-facing surfaces
edges.values.each do |e|
- next unless e.key?(:psi)
- next unless e.key?(:set)
- v = e[:psi].values.max
- set = e[:set]
- t = e[:psi].key(v)
- l = e[:length]
- l *= e[:mult] if e.key?(:mult)
- edge = { psi: set, type: t, length: l, surfaces: e[:surfaces].keys }
+ next unless e.key?(:psi)
+ next unless e.key?(:set)
+
+ v = e[:psi].values.max
+ set = e[:set]
+ t = e[:psi].key(v)
+ l = e[:length]
+ l *= e[:mult] if e.key?(:mult)
+ edge = { psi: set, type: t, length: l, surfaces: e[:surfaces].keys }
+
edge[:v0x] = e[:v0].point.x
edge[:v0y] = e[:v0].point.y
edge[:v0z] = e[:v0].point.z
edge[:v1x] = e[:v1].point.x
edge[:v1y] = e[:v1].point.y
edge[:v1z] = e[:v1].point.z
json[:io][:edges] << edge
end
- empty = json[:io][:edges].empty?
- json[:io][:edges].sort_by { |e| [ e[:v0x], e[:v0y], e[:v0z],
- e[:v1x], e[:v1y], e[:v1z] ] } unless empty
- json[:io].delete(:edges) if empty
+ if json[:io][:edges].empty?
+ json[:io].delete(:edges)
+ else
+ json[:io][:edges].sort_by { |e| [ e[:v0x], e[:v0y], e[:v0z],
+ e[:v1x], e[:v1y], e[:v1z] ] }
+ end
# Populate UA' trade-off reference values (optional).
- ua = argh[:gen_ua] && argh[:ua_ref] && argh[:ua_ref] == "code (Quebec)"
- qc33(tbd[:surfaces], json[:psi], setpoints) if ua
+ if argh[:gen_ua] && argh[:ua_ref]
+ case argh[:ua_ref]
+ when "code (Quebec)"
+ qc33(tbd[:surfaces], json[:psi], argh[:setpoints])
+ end
+ end
- tbd[:io] = json[:io]
+ tbd[:io ] = json[:io ]
+ argh[:io ] = tbd[:io ]
+ argh[:surfaces] = tbd[:surfaces]
+ argh[:version ] = model.getVersion.versionIdentifier
tbd
end
##
- # TBD exit strategy for OpenStudio Measures. May write out TBD model
- # content/results if requested (see argh). Always writes out minimal logs,
- # (see tbd.out.json).
+ # Exits TBD Measures. Writes out TBD model content and results if requested.
+ # Always writes out minimal logs (see "tbd.out.json" file).
#
# @param runner [Runner] OpenStudio Measure runner
- # @param argh [Hash] TBD arguments
+ # @param [Hash] argh TBD arguments
+ # @option argh [Hash] :io TBD input/output variables (see TBD JSON schema)
+ # @option argh [Hash] :surfaces TBD surfaces (keys: Openstudio surface names)
+ # @option argh [#to_s] :seed OpenStudio file, e.g. "school23.osm"
+ # @option argh [#to_s] :version :version OpenStudio SDK, e.g. "3.6.1"
+ # @option argh [Bool] :gen_ua whether to generate a UA' report
+ # @option argh [#to_s] :ua_ref selected UA' ruleset
+ # @option argh [Bool] :setpoints whether OpenStudio model holds setpoints
+ # @option argh [Bool] :write_tbd whether to output a JSON file
+ # @option argh [Bool] :uprate_walls whether to uprate walls
+ # @option argh [Bool] :uprate_roofs whether to uprate roofs
+ # @option argh [Bool] :uprate_floors whether to uprate floors
+ # @option argh [#to_f] :wall_ut uprated wall Ut target in W/m2•K
+ # @option argh [#to_f] :roof_ut uprated roof Ut target in W/m2•K
+ # @option argh [#to_f] :floor_ut uprated floor Ut target in W/m2•K
+ # @option argh [#to_s] :wall_option wall construction to uprate (or "all")
+ # @option argh [#to_s] :roof_option roof construction to uprate (or "all")
+ # @option argh [#to_s] :floor_option floor construction to uprate (or "all")
+ # @option argh [#to_f] :wall_uo required wall Uo to achieve Ut in W/m2•K
+ # @option argh [#to_f] :roof_uo required roof Uo to achieve Ut in W/m2•K
+ # @option argh [#to_f] :floor_uo required floor Uo to achieve Ut in W/m2•K
#
- # @return [Bool] true if TBD Measure is successful
+ # @return [Bool] whether TBD Measure is successful (see logs)
def exit(runner = nil, argh = {})
# Generated files target a design context ( >= WARN ) ... change TBD log
# level for debugging purposes. By default, log status is set < DBG
# while log level is set @INF.
- state = msg(status)
- state = msg(INF) if status.zero?
+ groups = { wall: {}, roof: {}, floor: {} }
+ state = msg(status)
+ state = msg(INF) if status.zero?
argh = {} unless argh.is_a?(Hash)
argh[:io ] = nil unless argh.key?(:io)
argh[:surfaces] = nil unless argh.key?(:surfaces)
unless argh[:io] && argh[:surfaces]
state = "Halting all TBD processes, yet running OpenStudio"
- state = "Halting all TBD processes, and halting OpenStudio" if fatal?
+ state = "Halting all TBD processes, and halting OpenStudio" if fatal?
end
argh[:io ] = {} unless argh[:io]
argh[:seed ] = "" unless argh.key?(:seed )
argh[:version ] = "" unless argh.key?(:version )
@@ -2149,11 +3099,10 @@
argh[:floor_option ] = "" unless argh.key?(:floor_option )
argh[:wall_uo ] = nil unless argh.key?(:wall_ut )
argh[:roof_uo ] = nil unless argh.key?(:roof_ut )
argh[:floor_uo ] = nil unless argh.key?(:floor_ut )
- groups = { wall: {}, roof: {}, floor: {} }
groups[:wall ][:up] = argh[:uprate_walls ]
groups[:roof ][:up] = argh[:uprate_roofs ]
groups[:floor][:up] = argh[:uprate_floors]
groups[:wall ][:ut] = argh[:wall_ut ]
groups[:roof ][:ut] = argh[:roof_ut ]
@@ -2182,31 +3131,31 @@
next unless g[:uo]
next unless g[:uo].is_a?(Numeric)
uo = format("%.3f", g[:uo])
ut = format("%.3f", g[:ut])
- output = "An initial #{label.to_s} Uo of #{uo} W/m2•K is required to " \
+ output = "An initial #{label.to_s} Uo of #{uo} W/m2•K is required to " \
"achieve an overall Ut of #{ut} W/m2•K for #{g[:op]}"
u_t << output
runner.registerInfo(output)
end
tbd_log[:ut] = u_t unless u_t.empty?
ua_md_en = nil
ua_md_fr = nil
ua = nil
ok = argh[:surfaces] && argh[:gen_ua]
- ua = ua_summary(tbd_log[:date], argh) if ok
+ ua = ua_summary(tbd_log[:date], argh) if ok
unless fatal? || ua.nil? || ua.empty?
if ua.key?(:en)
if ua[:en].key?(:b1) || ua[:en].key?(:b2)
+ tbd_log[:ua] = {}
runner.registerInfo("-")
runner.registerInfo(ua[:model])
- tbd_log[:ua] = {}
- ua_md_en = ua_md(ua, :en)
- ua_md_fr = ua_md(ua, :fr)
+ ua_md_en = ua_md(ua, :en)
+ ua_md_fr = ua_md(ua, :fr)
end
if ua[:en].key?(:b1) && ua[:en][:b1].key?(:summary)
runner.registerInfo(" - #{ua[:en][:b1][:summary]}")
@@ -2235,13 +3184,13 @@
if argh[:surfaces]
argh[:surfaces].each do |id, surface|
next if fatal?
next unless surface.key?(:ratio)
- ratio = format("%4.1f", surface[:ratio])
- output = "RSi derated by #{ratio}% : #{id}"
+ ratio = format("%4.1f", surface[:ratio])
+ output = "RSi derated by #{ratio}% : #{id}"
results << output
runner.registerInfo(output)
end
end
@@ -2283,18 +3232,18 @@
out_dir = '.'
file_paths = runner.workflow.absoluteFilePaths
# 'Apply Measure Now' won't cp files from 1st path back to generated_files.
- match1 = /WorkingFiles/.match(file_paths[1].to_s)
- match2 = /files/.match(file_paths[1].to_s)
+ match1 = /WorkingFiles/.match(file_paths[1].to_s.strip)
+ match2 = /files/.match(file_paths[1].to_s.strip)
match = match1 || match2
- if file_paths.size >= 2 && File.exists?(file_paths[1].to_s) && match
- out_dir = file_paths[1].to_s
- elsif !file_paths.empty? && File.exists?(file_paths.first.to_s)
- out_dir = file_paths.first.to_s
+ if file_paths.size >= 2 && File.exists?(file_paths[1].to_s.strip) && match
+ out_dir = file_paths[1].to_s.strip
+ elsif !file_paths.empty? && File.exists?(file_paths.first.to_s.strip)
+ out_dir = file_paths.first.to_s.strip
end
out_path = File.join(out_dir, "tbd.out.json")
File.open(out_path, 'w') do |file|
@@ -2305,10 +3254,10 @@
rescue StandardError
file.flush
end
end
- unless TBD.fatal? || ua.nil? || ua.empty?
+ unless fatal? || ua.nil? || ua.empty?
unless ua_md_en.nil? || ua_md_en.empty?
ua_path = File.join(out_dir, "ua_en.md")
File.open(ua_path, 'w') do |file|
file.puts ua_md_en