Class: BTAP::Bridging

Inherits:
Object
  • Object
show all
Extended by:
BridgingData
Defined in:
lib/openstudio-standards/btap/bridging.rb

Overview

—- —- —- —- —- —- —- —- —- —- —- —- —- —- —- #

Constant Summary collapse

TOL =
TBD::TOL
TOL2 =
TBD::TOL2
DBG =
TBD::DBG
INF =
TBD::INF
WRN =
TBD::WRN
ERR =
TBD::ERR
FTL =
TBD::FTL

Constants included from BridgingData

BTAP::BridgingData::FLOOR, BTAP::BridgingData::MASS2, BTAP::BridgingData::MASS2_BAD, BTAP::BridgingData::MASS2_GOOD, BTAP::BridgingData::MASS4, BTAP::BridgingData::MASS4_BAD, BTAP::BridgingData::MASS4_GOOD, BTAP::BridgingData::MASS8, BTAP::BridgingData::MASS8_BAD, BTAP::BridgingData::MASS8_GOOD, BTAP::BridgingData::MASSB, BTAP::BridgingData::MASSB_BAD, BTAP::BridgingData::MASSB_GOOD, BTAP::BridgingData::ROOF, BTAP::BridgingData::STEL1, BTAP::BridgingData::STEL1_BAD, BTAP::BridgingData::STEL1_GOOD, BTAP::BridgingData::STEL2, BTAP::BridgingData::STEL2_BAD, BTAP::BridgingData::STEL2_GOOD, BTAP::BridgingData::WOOD5, BTAP::BridgingData::WOOD5_BAD, BTAP::BridgingData::WOOD5_GOOD, BTAP::BridgingData::WOOD7, BTAP::BridgingData::WOOD7_BAD, BTAP::BridgingData::WOOD7_GOOD

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from BridgingData

costed_assembly, costed_uo, data, extended, lowest_uo, set

Constructor Details

#initialize(model = nil, argh = {}) ⇒ Bridging

Initializes OpenStudio model-specific BTAP/TBD data - uprates/derates.

Parameters:

Options Hash (argh):

  • structure (BTAP::Structure)

    a BTAP STRUCTURE object

  • walls (Hash)

    exterior wall parameters e.g. :uo, :ut

  • floors (Hash)

    exposed floor parameters e.g. :uo, :ut

  • roofs (Hash)

    exterior roof parameters e.g. :uo, :ut

  • quality (:good, :bad)

    derating option (if not uprating)

  • interpolate (Boolean)

    if TBD interpolates among Uo (uprate)



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
# File 'lib/openstudio-standards/btap/bridging.rb', line 577

def initialize(model = nil, argh = {})
  btp       = BTAP::Resources::Envelope::Constructions # alias
  mth       = "BTAP::Bridging::#{__callee__}"
  @model    = {}
  @tally    = {}
  @feedback = {logs: []}
  lgs       = @feedback[:logs]

  # Populate and validate BTAP/TBD & OpenStudio model parameters. This does
  # a safe TBD trial run, returning true if successful. If false, TBD leaves
  # the model unaltered. Check @feedback logs.
  return unless self.populate(model, argh)

  # Initialize loop controls and flags.
  initial  = true
  complies = false
  comply   = {} # specific to :walls, :floors & :roofs (if uprating)
  perform  = :lp
  quality  = argh[:quality] == :good ? :good : :bad
  combo    = "#{perform.to_s}_#{quality.to_s}".to_sym # e.g. :lp_bad
  args     = {} # TBD's own argument hash

  # Initialize surface types & TBD arguments for iterative uprating runs.
  @model[:stypes].each do |stypes|
    next unless argh[stypes].key?(:ut)

    stype  = stypes.to_s.chop
    uprate = "uprate_#{stypes.to_s}".to_sym
    option = "#{stype}_option".to_sym
    ut     = "#{stype}_ut".to_sym

    args[uprate] = true
    args[option] = "ALL #{stype} constructions"
    args[ut    ] = argh[stypes][:ut]

    comply[stypes] = false
  end

  # Building-wide PSI set.
  @model[:constructions].values.each do |v|
    args[:io_path] = v[combo] if v.key?(combo)
  end

  return false if args[:io_path].nil?

  args[:option ] = ""

  loop do
    if initial
      initial = false
    else
      # Subsequent uprating runs. Upgrade technologies. Reset TBD args.
      if quality == :bad
        quality = :good
        combo   = "#{perform.to_s}_#{quality.to_s}".to_sym

        @model[:constructions].values.each do |v|
          args[:io_path] = v[combo] if v.key?(combo)
        end
      elsif perform == :lp
        # Switch 'perform' from :lp to :hp - reset quality to :bad.
        perform = :hp
        quality = :bad
        combo   = "#{perform.to_s}_#{quality.to_s}".to_sym

        @model[:constructions].values.each do |v|
          args[:io_path] = v[combo] if v.key?(combo)
        end
      end

      # Delete previously-generated TBD args Uo key/value pairs.
      @model[:stypes].each do |stypes|
        next unless comply.key?(stypes)

        uo = "#{stypes.to_s.chop}_uo".to_sym
        args.delete(uo) if args.key?(uo)
      end
    end

    # Run TBD on cloned OpenStudio model - compliant combo?
    mdl = OpenStudio::Model::Model.new
    mdl.addObjects(model.toIdfFile.objects)
    TBD.clean!

    # fil = File.join("/Users/rd2/Desktop/test.osm")
    # mdl.save(fil, true)

    res = TBD.process(mdl, args)

    # Halt all processes if fatal errors raised by TBD (e.g. badly formatted
    # TBD arguments, poorly-structured OpenStudio models).
    if TBD.fatal?
      TBD.logs.each { |lg| lgs << lg[:message] if lg[:level] == TBD::FTL }
      break
    end

    complies = true
    # Check if TBD-uprated Uo factors are valid: TBD args hash holds (new)
    # uprated Uo keys/values for :walls, :floors and/or :roofs if uprating
    # is successful. In most cases, uprating tends to fail for wall
    # constructions rather than roof or floor constructions, due to the
    # typically larger density of linear thermal bridging per surface area
    # Yet even if all constructions were successfully uprated by TBD, one
    # must then determine if BTAP holds admissible (i.e. costed) assembly
    # variants with corresponding Uo factors. If TBD-uprated Uo factors are
    # lower than any of these admissible BTAP Uo factors, then no
    # commercially available solution can been identified.
    @model[:stypes].each do |stypes|
      next unless comply.key?(stypes) # true only if uprating

      # TBD-estimated Uo target to meet NECB-required Ut - nil if invalid.
      stype_uo = "#{stypes.to_s.chop}_uo".to_sym
      target   = args.key?(stype_uo) ? args[stype_uo] : nil
      assembly = self.costed_assembly(argh[:structure], stypes, perform)

      uo = target ? self.costed_uo(assembly, target) : nil

      if uo
        uo = target if argh[:interpolate]
        comply[stypes] = true
      else
        uo = self.lowest_uo(assembly)
        comply[stypes] = false
      end

      @model[:constructions].each do |lc, v|
        next unless v[:stypes] == stypes

        v[:uo] = uo
        v[:compliant] = comply[stypes]
      end

      complies = false unless comply[stypes]
    end

    # Exit if successful or if final BTAP uprating option.
    break if combo == :hp_good
    break if complies
  end

  # Post-loop steps (if uprating).
  @model[:stypes].each do |stypes|
    next unless comply.key?(stypes) # true only if uprating

    # Cancel uprating request before final derating.
    stype  = stypes.to_s.chop
    uprate = "uprate_#{stypes.to_s}".to_sym
    option = "#{stype}_option".to_sym
    ut     = "#{stype}_ut".to_sym
    args.delete(uprate)
    args.delete(option)
    args.delete(ut)

    # Reset uprated Uo factor for each 'deratable' construction.
    @model[:constructions].each do |lc, v|
      next unless v[:stypes] == stypes

      v[:r] = TBD.resetUo(lc, v[:filmRSI], v[:index], v[:uo])
    end
  end

  @model[:comply  ] = comply
  @model[:complies] = complies
  @model[:perform ] = perform
  @model[:quality ] = quality
  @model[:combo   ] = combo

  # Run "process" TBD one last time, on "model" (not cloned "mdl").
  TBD.clean!
  res = TBD.process(model, args)

  @model[:io      ] = res[:io      ] # TBD outputs (i.e. "tbd.out.json")
  @model[:surfaces] = res[:surfaces] # TBD derated surface data
  @model[:argh    ] = argh           # method argument hash
  @model[:args    ] = args           # last TBD inputs (i.e. "tbd.json")

  self.gen_tallies                   # tallies for BTAP costing
  self.gen_feedback                  # log success messages for BTAP
end

Instance Attribute Details

#feedbackHash (readonly)

Returns logged messages TBD reports back to BTAP.

Returns:

  • (Hash)

    logged messages TBD reports back to BTAP



561
562
563
# File 'lib/openstudio-standards/btap/bridging.rb', line 561

def feedback
  @feedback
end

#modelHash (readonly)

Returns BTAP/TBD hash, specific to an OpenStudio model.

Returns:

  • (Hash)

    BTAP/TBD hash, specific to an OpenStudio model



558
559
560
# File 'lib/openstudio-standards/btap/bridging.rb', line 558

def model
  @model
end

#tallyHash (readonly)

Returns TBD tallies e.g. total lengths of linear thermal bridges.

Returns:

  • (Hash)

    TBD tallies e.g. total lengths of linear thermal bridges



564
565
566
# File 'lib/openstudio-standards/btap/bridging.rb', line 564

def tally
  @tally
end

Instance Method Details

#gen_feedbackBoolean

Generate BTAP/TBD post-processing feedback.

Returns:

  • (Boolean)

    true if valid BTAP/TBD model



1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
# File 'lib/openstudio-standards/btap/bridging.rb', line 1065

def gen_feedback
  lgs = @feedback[:logs]
  return false unless @model.key?(:complies) # all model constructions
  return false unless @model.key?(:comply)   # surface type specific ...
  return false unless @model.key?(:argh)     # BTAP/TBD inputs + ouputs
  return false unless @model.key?(:stypes)   # :walls, :roofs, :floors

  argh = @model[:argh]

  # Uprating. Report first on surface types (compliant or not).
  @model[:stypes].each do |stypes|
    next unless @model[:comply].key?(stypes)

    ut  = format("%.3f", argh[stypes][:ut])
    lg  = @model[:comply][stypes] ? "Compliant " : "Non-compliant "
    lg += "#{stypes}: Ut #{ut} W/m2.K"
    lgs << lg

    # Report then on required Uo factor per construction (compliant or not).
    @model[:constructions].each do |lc, v|
      next unless v.key?(:stypes)
      next unless v.key?(:uo)
      next unless v.key?(:compliant)
      next unless v.key?(:surfaces)
      next unless v[:stypes  ] == stypes
      next     if v[:surfaces].empty?

      uo  = format("%.3f", v[:uo])
      lg  = v[:compliant] ? "   Compliant " : "   Non-compliant "
      lg += "#{lc.nameString} Uo #{uo} (W/K.m2)"
      lgs << lg
    end
  end

  # Summary of TBD-derated constructions.
  @model[:osm].getSurfaces.each do |s|
    next if s.construction.empty?
    next if s.construction.get.to_LayeredConstruction.empty?

    lc = s.construction.get.to_LayeredConstruction.get
    id = lc.nameString
    next unless id.include?(" c tbd")

    rsi  = TBD.rsi(lc, s.filmResistance)
    usi  = format("%.3f", 1/rsi)
    rsi  = format("%.1f", rsi)
    area = format("%.1f", lc.getNetArea) + " m2"

    lgs << "~ '#{id}' derated Rsi: #{rsi} [Usi #{usi} x #{area}]"
  end

  # Log PSI factor tallies (per thermal bridge type).
  if @tally.key?(:edges)
    @tally[:edges].each do |type, e|
      next if type == :transition

      lgs << "# '#{type}' (#{e.size}x):"

      e.each do |psi, length|
        l = format("%.2f", length)
        lgs << "... PSI set '#{psi}' : #{l} m"
      end
    end
  end

  true
end

#gen_talliesBoolean

Generate BTAP/TBD tallies

Returns:

  • (Boolean)

    true if BTAP/TBD tally is successful



1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
# File 'lib/openstudio-standards/btap/bridging.rb', line 1036

def gen_tallies
  edges = {}
  return false unless @model.key?(:io)
  return false unless @model.key?(:constructions)
  return false unless @model[:io].key?(:edges)

  @model[:io][:edges].each do |e|
    # Content of TBD-generated 'edges' (hashes):
    #      psi: BTAP PSI set ID, e.g. "BTAP-ExteriorWall-Mass-6 good"
    #     type: thermal bridge type, e.g. :corner
    #   length: (in m)
    # surfaces: linked OpenStudio surface IDs
    edges[e[:type]]           = {} unless edges.key?(e[:type])
    edges[e[:type]][e[:psi]]  = 0  unless edges[e[:type]].key?(e[:psi])
    edges[e[:type]][e[:psi]] += e[:length]
  end

  return false if edges.empty?

  @tally[:edges] = edges
  @tally[:constructions] = @model[:constructions]

  true
end

#inputs(structure = nil, perform = :hp, quality = :good) ⇒ Hash

Generate TBD input hash.

Parameters:

  • structure (BTAP::Structure) (defaults to: nil)

    a BTAP STRUCTURE object

  • perform (Symbol) (defaults to: :hp)

    :lp or :hp wall variant

  • quality (Symbol) (defaults to: :good)

    :bad or :good PSI-factor

Returns:

  • (Hash)

    TBD inputs



1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
# File 'lib/openstudio-standards/btap/bridging.rb', line 1006

def inputs(structure = nil, perform = :hp, quality = :good)
  input   = {}
  psis    = {} # construction-specific PSI sets
  perform = :hp   unless [:lp, :hp].include?(perform)
  quality = :good unless [:bad, :good].include?(quality)

  # A single PSI set for the entire building, based strictly on exterior
  # wall selection. Adapt once BTAP::Structure supports STRUCTURE
  # assignements per OpenStudio's building-to-space hierarchy, e.g. "cmu"
  # gymnasium walls in an otherwise "steel"post/frame school. @todo
  assembly = self.costed_assembly(structure, :walls, perform)
  building_psi = self.set(assembly, quality)

  psis[ building_psi[:id] ] = building_psi

  # TBD JSON schema added as a reminder. No schema validation in BTAP.
  schema = "https://github.com/rd2/tbd/blob/master/tbd.schema.json"

  input[:schema     ] = schema
  input[:description] = "TBD input for BTAP" # append run # ?
  input[:psis       ] = psis.values          # maybe more than 1 in future
  input[:building   ] = { psi: building_psi[:id] }

  input
end

#populate(model = nil, argh = {}) ⇒ Boolean

Populates and validates BTAP/TBD & OpenStudio model parameters for thermal bridging. This also does a safe TBD trial run, returning true if successful. Check @feedback logs if false.

Parameters:

Options Hash (argh):

  • structure (BTAP::Structure)

    a BTAP STRUCTURE object

  • walls (Hash)

    exterior wall parameters e.g. :uo, :ut

  • floors (Hash)

    exposed floor parameters e.g. :uo, :ut

  • roofs (Hash)

    exterior roof parameters e.g. :uo, :ut

  • quality (Symbol)

    derating option (if not uprating)

  • interpolate (Boolean)

    if TBD should pick between Uo values

Returns:

  • (Boolean)

    true if valid (check @feedback logs if false)



772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
# File 'lib/openstudio-standards/btap/bridging.rb', line 772

def populate(model = nil, argh = {})
  mth  = "BTAP::Bridging::#{__callee__}"
  args = { option: "(non thermal bridging)" } # for initial TBD dry run
  lgs  = @feedback[:logs]
  cl   = OpenStudio::Model::LayeredConstruction

  unless model.is_a?(OpenStudio::Model::Model)
    lgs << "Invalid OpenStudio model to de/up-rate (#{mth})"
    return false
  end

  unless argh.is_a?(Hash)
    lgs << "Invalid BTAP/TBD argument Hash (#{mth})"
    return false
  end

  if argh.key?(:structure)
    unless argh[:structure].is_a?(BTAP::Structure)
      lgs << "Invalid BTAP::Structure (#{mth})"
      return false
    end
  else
    lgs << "Missing STRUCTURE key (#{mth})"
    return false
  end

  # Building-wide envelope (e.g. assemblies, U-factors, PSI-factors) options
  # depend on building structure-dependent features such as framing,
  # cladding, etc. This will need to adapt to upcoming story/space
  # construction/PSI customization - @todo.
  strc = argh[:structure]

  argh[:interpolate] = false unless argh.key?(:interpolate)
  argh[:interpolate] = false unless [true, false].include?(argh[:interpolate])

  [:walls, :floors, :roofs].each do |stypes|
    unless argh.key?(stypes)
      lgs << "Missing BTAP/TBD #{stypes} (#{mth})"
      return false
    end

    unless argh[stypes].key?(:uo)
      lgs << "Missing BTAP/TBD #{stypes} Uo (#{mth})"
      return false
    end

    uo = argh[stypes][:uo]

    unless uo.is_a?(Numeric) && uo.between?(TBD::UMIN, TBD::UMAX)
      lgs << "Invalid BTAP/TBD #{stypes} Uo (#{mth})"
      return false
    end

    next unless argh[stypes].key?(:ut)

    ut = argh[stypes][:ut]

    unless ut.is_a?(Numeric) && ut.between?(TBD::UMIN, TBD::UMAX)
      lgs << "Invalid BTAP/TBD #{stypes} Ut (#{mth})"
      return false
    end
  end

  # Run TBD on a cloned OpenStudio model (dry run).
  mdl = OpenStudio::Model::Model.new
  mdl.addObjects(model.toIdfFile.objects)
  TBD.clean!
  res = TBD.process(mdl, args)

  # TBD validation of OpenStudio model.
  if TBD.fatal? || TBD.error?
    lgs << "TBD-identified FATAL error(s):"     if TBD.fatal?
    lgs << "TBD-identified non-FATAL error(s):" if TBD.error?

    TBD.logs.each { |log| lgs << log[:message] }
    return false if TBD.fatal?
  end

  # Fetch number of stories in OpenStudio model.
  stories = model.getBuilding.standardsNumberOfAboveGroundStories
  stories = stories.get                  unless stories.empty?
  stories = model.getBuildingStorys.size unless stories.is_a?(Integer)

  # Story/space construction/PSI customization is yet to be implemented.
  # Keeping placeholders for now - @todo.
  @model[:stories] = stories.clamp(1, 999)
  @model[:spaces ] = {}

  # Initialize deratable opaque, layered constructions & surface types.
  @model[:constructions] = {}
  @model[:stypes       ] = []

  if res[:surfaces].nil?
    lgs << "No deratable surfaces in model (#{mth})"
    return false
  end

  # TBD surface objects hold certain attributes (keys) to signal if they're
  # deratable. Concentrating only on those. Relying on reported strings
  # (e.g. surface identifier) or integers (e.g. a layer :index) seems fine.
  # Yet referecing TBD-cloned OpenStudio objects (e.g. key :construction)
  # is a no-no (e.g. seg faults).
  res[:surfaces].each do |id, surface|
    next unless surface.key?(:type)      # :wall, :ceiling or :floor
    next unless surface.key?(:filmRSI)   # sum of air film resistances
    next unless surface.key?(:index)     # deratable layer index
    next unless surface.key?(:r)         # deratable layer RSi
    next unless surface.key?(:deratable) # true or false

    next unless surface[:deratable]
    next unless surface[:index    ]

    stypes = case surface[:type]
             when :wall    then :walls
             when :floor   then :floors
             when :ceiling then :roofs
             else ""
             end

    next if stypes.empty?

    # Track surface type.
    @model[:stypes] << stypes unless @model[:stypes].include?(stypes)

    # Track TBD-targeted constructions for uprating/derating.
    srf = model.getSurfaceByName(id)

    if srf.empty?
      lgs << "Mismatched surface: #{id} (#{mth})?"
      return false
    end

    srf = srf.get
    space = srf.space

    if space.empty?
      lgs << "Missing space: #{id} (#{mth})?"
      return false
    end

    space = space.get
    lc = srf.construction

    if lc.empty?
      lgs << "Mismatched construction: #{id} (#{mth})?"
      return false
    end

    lc = lc.get.to_LayeredConstruction

    if lc.empty?
      lgs << "Mismatched layered construction: #{id} (#{mth})?"
      return false
    end

    lc = lc.get

    unless @model[:constructions].key?(lc)
      @model[:constructions][lc]             = {}
      @model[:constructions][lc][:index    ] = surface[:index]   # material
      @model[:constructions][lc][:r        ] = surface[:r]       # material
      @model[:constructions][lc][:filmRSI  ] = surface[:filmRSI] # assembly
      @model[:constructions][lc][:uo       ] = nil               # assembly
      @model[:constructions][lc][:compliant] = nil               # assembly
      @model[:constructions][lc][:stypes   ] = []
      @model[:constructions][lc][:surfaces ] = []
      @model[:constructions][lc][:spaces   ] = []

      # Generate TBD input hashes for both :good & :bad PSI factor sets.
      # This depends solely on assigned wall constructions (e.g. steel- vs
      # wood-framed) - not roof or floor constructions. Until space- and
      # storey-specific structure/construction customization is enabled in
      # BTAP, this is set for the entire building. In other words - for now
      # - there should be a single assigned layered construction for all
      # walls in a BTAP-altered OpenStudio model.
      if stypes == :walls
        @model[:constructions][lc][:lp_bad ] = self.inputs(strc, :lp, :bad )
        @model[:constructions][lc][:lp_good] = self.inputs(strc, :lp, :good)
        @model[:constructions][lc][:hp_bad ] = self.inputs(strc, :hp, :bad )
        @model[:constructions][lc][:hp_good] = self.inputs(strc, :hp, :good)
      end
    end

    # Select lowest applicable air film resistances (given surface slope).
    film = [@model[:constructions][lc][:filmRSI], surface[:filmRSI]].min

    @model[:constructions][lc][:filmRSI ] = film
    @model[:constructions][lc][:stypes  ] << stypes           # 1x
    @model[:constructions][lc][:surfaces] << id               # many
    @model[:constructions][lc][:spaces  ] << space.nameString # less
  end

  # Loop through all tracked deratable constructions. Ensure a single
  # surface type per construction. Ensure at least one wall construction.
  @model[:constructions].values.each { |v| v[:stypes].uniq! }
  nb = 0

  @model[:constructions].each do |lc, v|
    if v[:stypes].size != 1
      lgs << "Multiple surface types per construction (#{mth})?"
      return false
    else
      v[:stypes] = v[:stypes].first

      # Assign construction for each deratable surface.
      v[:surfaces].each do |id|
        surface = model.getSurfaceByName(id)
        next if surface.empty?

        surface.get.setConstruction(lc)
      end
    end

    nb += 1 if v[:stypes] == :walls
  end

  if nb < 1
    lgs << "No deratable walls (#{mth})?"
    return false
  end

  @model[:osm] = model

  true
end