osut 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/pull_request.yml +17 -1
- data/README.md +11 -12
- data/lib/osut/utils.rb +2491 -764
- data/lib/osut/version.rb +1 -1
- data/osut.gemspec +3 -4
- metadata +8 -8
    
        data/lib/osut/utils.rb
    CHANGED
    
    | @@ -31,23 +31,1094 @@ | |
| 31 31 | 
             
            require "openstudio"
         | 
| 32 32 |  | 
| 33 33 | 
             
            module OSut
         | 
| 34 | 
            -
               | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
               | 
| 38 | 
            -
               | 
| 39 | 
            -
               | 
| 40 | 
            -
               | 
| 41 | 
            -
               | 
| 42 | 
            -
               | 
| 43 | 
            -
               | 
| 44 | 
            -
               | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
               | 
| 48 | 
            -
             | 
| 34 | 
            +
              # DEBUG for devs; WARN/ERROR for users (bad OS input), see OSlg
         | 
| 35 | 
            +
              extend OSlg
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              TOL  = 0.01         # default distance tolerance (m)
         | 
| 38 | 
            +
              TOL2 = TOL * TOL    # default area tolerance (m2)
         | 
| 39 | 
            +
              DBG  = OSlg::DEBUG  # see github.com/rd2/oslg
         | 
| 40 | 
            +
              INF  = OSlg::INFO   # see github.com/rd2/oslg
         | 
| 41 | 
            +
              WRN  = OSlg::WARN   # see github.com/rd2/oslg
         | 
| 42 | 
            +
              ERR  = OSlg::ERROR  # see github.com/rd2/oslg
         | 
| 43 | 
            +
              FTL  = OSlg::FATAL  # see github.com/rd2/oslg
         | 
| 44 | 
            +
              NS   = "nameString" # OpenStudio object identifier method
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              HEAD = 2.032 # standard 80" door
         | 
| 47 | 
            +
              SILL = 0.762 # standard 30" window sill
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              # General surface orientations (see facets method)
         | 
| 50 | 
            +
              SIDZ = [:bottom, # e.g. ground-facing, exposed floros
         | 
| 51 | 
            +
                         :top, # e.g. roof/ceiling
         | 
| 52 | 
            +
                       :north, # NORTH
         | 
| 53 | 
            +
                        :east, # EAST
         | 
| 54 | 
            +
                       :south, # SOUTH
         | 
| 55 | 
            +
                        :west  # WEST
         | 
| 56 | 
            +
                      ].freeze
         | 
| 57 | 
            +
             | 
| 58 | 
            +
              # This first set of utilities support OpenStudio materials, constructions,
         | 
| 59 | 
            +
              # construction sets, etc. If relying on default StandardOpaqueMaterial:
         | 
| 60 | 
            +
              #   - roughness            (rgh) : "Smooth"
         | 
| 61 | 
            +
              #   - thickness                  :    0.1 m
         | 
| 62 | 
            +
              #   - thermal conductivity (k  ) :    0.1 W/m.K
         | 
| 63 | 
            +
              #   - density              (rho) :    0.1 kg/m3
         | 
| 64 | 
            +
              #   - specific heat        (cp ) : 1400.0 J/kg.K
         | 
| 65 | 
            +
              #
         | 
| 66 | 
            +
              #   https://s3.amazonaws.com/openstudio-sdk-documentation/cpp/
         | 
| 67 | 
            +
              #   OpenStudio-3.6.1-doc/model/html/
         | 
| 68 | 
            +
              #   classopenstudio_1_1model_1_1_standard_opaque_material.html
         | 
| 69 | 
            +
              #
         | 
| 70 | 
            +
              # ... apart from surface roughness, rarely would these material properties be
         | 
| 71 | 
            +
              # suitable - and are therefore explicitely set below. On roughness:
         | 
| 72 | 
            +
              #   - "Very Rough"    : stucco
         | 
| 73 | 
            +
              #   - "Rough"	        : brick
         | 
| 74 | 
            +
              #   - "Medium Rough"  : concrete
         | 
| 75 | 
            +
              #   - "Medium Smooth" : clear pine
         | 
| 76 | 
            +
              #   - "Smooth"        : smooth plaster
         | 
| 77 | 
            +
              #   - "Very Smooth"   : glass
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              # thermal mass categories (e.g. exterior cladding, interior finish, framing)
         | 
| 80 | 
            +
              @@mass = [
         | 
| 81 | 
            +
                  :none, # token for 'no user selection', resort to defaults
         | 
| 82 | 
            +
                 :light, # e.g. 16mm drywall interior
         | 
| 83 | 
            +
                :medium, # e.g. 100mm brick cladding
         | 
| 84 | 
            +
                 :heavy  # e.g. 200mm poured concrete
         | 
| 85 | 
            +
              ].freeze
         | 
| 86 | 
            +
             | 
| 87 | 
            +
              # basic materials (StandardOpaqueMaterials only)
         | 
| 88 | 
            +
              @@mats = {
         | 
| 89 | 
            +
                    sand: {},
         | 
| 90 | 
            +
                concrete: {},
         | 
| 91 | 
            +
                   brick: {},
         | 
| 92 | 
            +
                cladding: {}, # e.g. lightweight cladding over furring
         | 
| 93 | 
            +
               sheathing: {}, # e.g. plywood
         | 
| 94 | 
            +
                 polyiso: {}, # e.g. polyisocyanurate panel (or similar)
         | 
| 95 | 
            +
               cellulose: {}, # e.g. blown, dry/stabilized fiber
         | 
| 96 | 
            +
                 mineral: {}, # e.g. semi-rigid rock wool insulation
         | 
| 97 | 
            +
                 drywall: {},
         | 
| 98 | 
            +
                    door: {}  # single composite material (45mm insulated steel door)
         | 
| 99 | 
            +
              }.freeze
         | 
| 100 | 
            +
             | 
| 101 | 
            +
              # default inside+outside air film resistances (m2.K/W)
         | 
| 102 | 
            +
              @@film = {
         | 
| 103 | 
            +
                  shading: 0.000, # NA
         | 
| 104 | 
            +
                partition: 0.000,
         | 
| 105 | 
            +
                     wall: 0.150,
         | 
| 106 | 
            +
                     roof: 0.140,
         | 
| 107 | 
            +
                    floor: 0.190,
         | 
| 108 | 
            +
                 basement: 0.120,
         | 
| 109 | 
            +
                     slab: 0.160,
         | 
| 110 | 
            +
                     door: 0.150,
         | 
| 111 | 
            +
                   window: 0.150, # ignored if SimpleGlazingMaterial
         | 
| 112 | 
            +
                 skylight: 0.140  # ignored if SimpleGlazingMaterial
         | 
| 113 | 
            +
              }.freeze
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              # default (~1980s) envelope Uo (W/m2•K), based on surface type
         | 
| 116 | 
            +
              @@uo = {
         | 
| 117 | 
            +
                  shading: 0.000, # N/A
         | 
| 118 | 
            +
                partition: 0.000, # N/A
         | 
| 119 | 
            +
                     wall: 0.384, # rated Ro ~14.8 hr•ft2F/Btu
         | 
| 120 | 
            +
                     roof: 0.327, # rated Ro ~17.6 hr•ft2F/Btu
         | 
| 121 | 
            +
                    floor: 0.317, # rated Ro ~17.9 hr•ft2F/Btu (exposed floor)
         | 
| 122 | 
            +
                 basement: 0.000, # uninsulated
         | 
| 123 | 
            +
                     slab: 0.000, # uninsulated
         | 
| 124 | 
            +
                     door: 1.800, # insulated, unglazed steel door (single layer)
         | 
| 125 | 
            +
                   window: 2.800, # e.g. patio doors (simple glazing)
         | 
| 126 | 
            +
                 skylight: 3.500  # all skylight technologies
         | 
| 127 | 
            +
              }.freeze
         | 
| 128 | 
            +
             | 
| 129 | 
            +
              # Standard opaque materials, taken from a variety of sources (e.g. energy
         | 
| 130 | 
            +
              # codes, NREL's BCL). Material identifiers are symbols, e.g.:
         | 
| 131 | 
            +
              #   - :brick
         | 
| 132 | 
            +
              #   - :sand
         | 
| 133 | 
            +
              #   - :concrete
         | 
| 134 | 
            +
              #
         | 
| 135 | 
            +
              # Material properties remain largely constant between projects. What does
         | 
| 136 | 
            +
              # tend to vary (between projects) are thicknesses. Actual OpenStudio opaque
         | 
| 137 | 
            +
              # material objects can be (re)set in more than one way by class methods.
         | 
| 138 | 
            +
              # In genConstruction, OpenStudio object identifiers are later suffixed with
         | 
| 139 | 
            +
              # actual material thicknesses, in mm, e.g.:
         | 
| 140 | 
            +
              #   - "concrete200" : 200mm concrete slab
         | 
| 141 | 
            +
              #   - "drywall13"   : 1/2" gypsum board
         | 
| 142 | 
            +
              #   - "drywall16"   : 5/8" gypsum board
         | 
| 143 | 
            +
              #
         | 
| 144 | 
            +
              # Surface absorptances are also defaulted in OpenStudio:
         | 
| 145 | 
            +
              #   - thermal, long-wave   (thm) : 90%
         | 
| 146 | 
            +
              #   - solar                (sol) : 70%
         | 
| 147 | 
            +
              #   - visible              (vis) : 70%
         | 
| 148 | 
            +
              #
         | 
| 149 | 
            +
              # These can also be explicitly set, here (e.g. a redundant 'sand' example):
         | 
| 150 | 
            +
              @@mats[:sand     ][:rgh] = "Rough"
         | 
| 151 | 
            +
              @@mats[:sand     ][:k  ] =    1.290
         | 
| 152 | 
            +
              @@mats[:sand     ][:rho] = 2240.000
         | 
| 153 | 
            +
              @@mats[:sand     ][:cp ] =  830.000
         | 
| 154 | 
            +
              @@mats[:sand     ][:thm] =    0.900
         | 
| 155 | 
            +
              @@mats[:sand     ][:sol] =    0.700
         | 
| 156 | 
            +
              @@mats[:sand     ][:vis] =    0.700
         | 
| 157 | 
            +
             | 
| 158 | 
            +
              @@mats[:concrete ][:rgh] = "MediumRough"
         | 
| 159 | 
            +
              @@mats[:concrete ][:k  ] =    1.730
         | 
| 160 | 
            +
              @@mats[:concrete ][:rho] = 2240.000
         | 
| 161 | 
            +
              @@mats[:concrete ][:cp ] =  830.000
         | 
| 162 | 
            +
             | 
| 163 | 
            +
              @@mats[:brick    ][:rgh] = "Rough"
         | 
| 164 | 
            +
              @@mats[:brick    ][:k  ] =    0.675
         | 
| 165 | 
            +
              @@mats[:brick    ][:rho] = 1600.000
         | 
| 166 | 
            +
              @@mats[:brick    ][:cp ] =  790.000
         | 
| 167 | 
            +
             | 
| 168 | 
            +
              @@mats[:cladding ][:rgh] = "MediumSmooth"
         | 
| 169 | 
            +
              @@mats[:cladding ][:k  ] =    0.115
         | 
| 170 | 
            +
              @@mats[:cladding ][:rho] =  540.000
         | 
| 171 | 
            +
              @@mats[:cladding ][:cp ] = 1200.000
         | 
| 172 | 
            +
             | 
| 173 | 
            +
              @@mats[:sheathing][:k  ] =    0.160
         | 
| 174 | 
            +
              @@mats[:sheathing][:rho] =  545.000
         | 
| 175 | 
            +
              @@mats[:sheathing][:cp ] = 1210.000
         | 
| 176 | 
            +
             | 
| 177 | 
            +
              @@mats[:polyiso  ][:k  ] =    0.025
         | 
| 178 | 
            +
              @@mats[:polyiso  ][:rho] =   25.000
         | 
| 179 | 
            +
              @@mats[:polyiso  ][:cp ] = 1590.000
         | 
| 180 | 
            +
             | 
| 181 | 
            +
              @@mats[:cellulose][:rgh] = "VeryRough"
         | 
| 182 | 
            +
              @@mats[:cellulose][:k  ] =    0.050
         | 
| 183 | 
            +
              @@mats[:cellulose][:rho] =   80.000
         | 
| 184 | 
            +
              @@mats[:cellulose][:cp ] =  835.000
         | 
| 185 | 
            +
             | 
| 186 | 
            +
              @@mats[:mineral  ][:k  ] =    0.050
         | 
| 187 | 
            +
              @@mats[:mineral  ][:rho] =   19.000
         | 
| 188 | 
            +
              @@mats[:mineral  ][:cp ] =  960.000
         | 
| 189 | 
            +
             | 
| 190 | 
            +
              @@mats[:drywall  ][:k  ] =    0.160
         | 
| 191 | 
            +
              @@mats[:drywall  ][:rho] =  785.000
         | 
| 192 | 
            +
              @@mats[:drywall  ][:cp ] = 1090.000
         | 
| 193 | 
            +
             | 
| 194 | 
            +
              @@mats[:door     ][:rgh] = "MediumSmooth"
         | 
| 195 | 
            +
              @@mats[:door     ][:k  ] =    0.080
         | 
| 196 | 
            +
              @@mats[:door     ][:rho] =  600.000
         | 
| 197 | 
            +
              @@mats[:door     ][:cp ] = 1000.000
         | 
| 198 | 
            +
             | 
| 199 | 
            +
              ##
         | 
| 200 | 
            +
              # Generates an OpenStudio multilayered construction; materials if needed.
         | 
| 201 | 
            +
              #
         | 
| 202 | 
            +
              # @param model [OpenStudio::Model::Model] a model
         | 
| 203 | 
            +
              # @param [Hash] specs OpenStudio construction specifications
         | 
| 204 | 
            +
              # @option specs [#to_s] :id ("") construction identifier
         | 
| 205 | 
            +
              # @option specs [Symbol] :type (:wall), see @@uo
         | 
| 206 | 
            +
              # @option specs [Numeric] :uo clear-field Uo, in W/m2.K, see @@uo
         | 
| 207 | 
            +
              # @option specs [Symbol] :clad (:light) exterior cladding, see @@mass
         | 
| 208 | 
            +
              # @option specs [Symbol] :frame (:light) assembly framing, see @@mass
         | 
| 209 | 
            +
              # @option specs [Symbol] :finish (:light) interior finishing, see @@mass
         | 
| 210 | 
            +
              #
         | 
| 211 | 
            +
              # @return [OpenStudio::Model::Construction] generated construction
         | 
| 212 | 
            +
              # @return [nil] if invalid inputs (see logs)
         | 
| 213 | 
            +
              def genConstruction(model = nil, specs = {})
         | 
| 214 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 215 | 
            +
                cl1 = OpenStudio::Model::Model
         | 
| 216 | 
            +
                cl2 = Hash
         | 
| 217 | 
            +
                return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
         | 
| 218 | 
            +
                return mismatch("specs", specs, cl2, mth) unless specs.is_a?(cl2)
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                specs[:id  ] = ""    unless specs.key?(:id  )
         | 
| 221 | 
            +
                specs[:type] = :wall unless specs.key?(:type)
         | 
| 222 | 
            +
                chk = @@uo.keys.include?(specs[:type])
         | 
| 223 | 
            +
                return invalid("surface type", mth, 2, ERR) unless chk
         | 
| 224 | 
            +
             | 
| 225 | 
            +
                id = trim(specs[:id])
         | 
| 226 | 
            +
                id = "OSut|CON|#{specs[:type]}"       if id.empty?
         | 
| 227 | 
            +
                specs[:uo] = @@uo[ specs[:type] ] unless specs.key?(:uo)
         | 
| 228 | 
            +
                u = specs[:uo]
         | 
| 229 | 
            +
                return mismatch("#{id} Uo", u, Numeric, mth)  unless u.is_a?(Numeric)
         | 
| 230 | 
            +
                return invalid("#{id} Uo (> 5.678)", mth, 2, ERR) if u > 5.678
         | 
| 231 | 
            +
                return negative("#{id} Uo"         , mth,    ERR) if u < 0
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                # Optional specs. Log/reset if invalid.
         | 
| 234 | 
            +
                specs[:clad  ] = :light             unless specs.key?(:clad  ) # exterior
         | 
| 235 | 
            +
                specs[:frame ] = :light             unless specs.key?(:frame )
         | 
| 236 | 
            +
                specs[:finish] = :light             unless specs.key?(:finish) # interior
         | 
| 237 | 
            +
                log(WRN, "Reset to light cladding") unless @@mass.include?(specs[:clad  ])
         | 
| 238 | 
            +
                log(WRN, "Reset to light framing" ) unless @@mass.include?(specs[:frame ])
         | 
| 239 | 
            +
                log(WRN, "Reset to light finish"  ) unless @@mass.include?(specs[:finish])
         | 
| 240 | 
            +
                specs[:clad  ] = :light             unless @@mass.include?(specs[:clad  ])
         | 
| 241 | 
            +
                specs[:frame ] = :light             unless @@mass.include?(specs[:frame ])
         | 
| 242 | 
            +
                specs[:finish] = :light             unless @@mass.include?(specs[:finish])
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                film = @@film[ specs[:type] ]
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                # Layered assembly (max 4 layers):
         | 
| 247 | 
            +
                #   - cladding
         | 
| 248 | 
            +
                #   - intermediate sheathing
         | 
| 249 | 
            +
                #   - composite insulating/framing
         | 
| 250 | 
            +
                #   - interior finish
         | 
| 251 | 
            +
                a = {clad: {}, sheath: {}, compo: {}, finish: {}, glazing: {}}
         | 
| 252 | 
            +
             | 
| 253 | 
            +
                case specs[:type]
         | 
| 254 | 
            +
                when :shading
         | 
| 255 | 
            +
                  mt = :sheathing
         | 
| 256 | 
            +
                  d  = 0.015
         | 
| 257 | 
            +
                  a[:compo][:mat] = @@mats[mt]
         | 
| 258 | 
            +
                  a[:compo][:d  ] = d
         | 
| 259 | 
            +
                  a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 260 | 
            +
                when :partition
         | 
| 261 | 
            +
                  d  = 0.015
         | 
| 262 | 
            +
                  mt = :drywall
         | 
| 263 | 
            +
                  a[:clad][:mat] = @@mats[mt]
         | 
| 264 | 
            +
                  a[:clad][:d  ] = d
         | 
| 265 | 
            +
                  a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 266 | 
            +
             | 
| 267 | 
            +
                  mt = :sheathing
         | 
| 268 | 
            +
                  a[:compo][:mat] = @@mats[mt]
         | 
| 269 | 
            +
                  a[:compo][:d  ] = d
         | 
| 270 | 
            +
                  a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 271 | 
            +
             | 
| 272 | 
            +
                  mt = :drywall
         | 
| 273 | 
            +
                  a[:finish][:mat] = @@mats[mt]
         | 
| 274 | 
            +
                  a[:finish][:d  ] = d
         | 
| 275 | 
            +
                  a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 276 | 
            +
                when :wall
         | 
| 277 | 
            +
                  unless specs[:clad] == :none
         | 
| 278 | 
            +
                    mt = :cladding
         | 
| 279 | 
            +
                    mt = :brick    if specs[:clad] == :medium
         | 
| 280 | 
            +
                    mt = :concrete if specs[:clad] == :heavy
         | 
| 281 | 
            +
                    d  = 0.100
         | 
| 282 | 
            +
                    d  = 0.015     if specs[:clad] == :light
         | 
| 283 | 
            +
                    a[:clad][:mat] = @@mats[mt]
         | 
| 284 | 
            +
                    a[:clad][:d  ] = d
         | 
| 285 | 
            +
                    a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 286 | 
            +
                  end
         | 
| 287 | 
            +
             | 
| 288 | 
            +
                  mt = :drywall
         | 
| 289 | 
            +
                  mt = :polyiso   if specs[:frame] == :medium
         | 
| 290 | 
            +
                  mt = :mineral   if specs[:frame] == :heavy
         | 
| 291 | 
            +
                  d  = 0.100
         | 
| 292 | 
            +
                  d  = 0.015      if specs[:frame] == :light
         | 
| 293 | 
            +
                  a[:sheath][:mat] = @@mats[mt]
         | 
| 294 | 
            +
                  a[:sheath][:d  ] = d
         | 
| 295 | 
            +
                  a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 296 | 
            +
             | 
| 297 | 
            +
                  mt = :concrete
         | 
| 298 | 
            +
                  mt = :mineral   if specs[:frame] == :light
         | 
| 299 | 
            +
                  d  = 0.100
         | 
| 300 | 
            +
                  d  = 0.200      if specs[:frame] == :heavy
         | 
| 301 | 
            +
                  a[:compo][:mat] = @@mats[mt]
         | 
| 302 | 
            +
                  a[:compo][:d  ] = d
         | 
| 303 | 
            +
                  a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                  unless specs[:finish] == :none
         | 
| 306 | 
            +
                    mt = :concrete
         | 
| 307 | 
            +
                    mt = :drywall   if specs[:finish] == :light
         | 
| 308 | 
            +
                    d  = 0.015
         | 
| 309 | 
            +
                    d  = 0.100      if specs[:finish] == :medium
         | 
| 310 | 
            +
                    d  = 0.200      if specs[:finish] == :heavy
         | 
| 311 | 
            +
                    a[:finish][:mat] = @@mats[mt]
         | 
| 312 | 
            +
                    a[:finish][:d  ] = d
         | 
| 313 | 
            +
                    a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 314 | 
            +
                  end
         | 
| 315 | 
            +
                when :roof
         | 
| 316 | 
            +
                  unless specs[:clad] == :none
         | 
| 317 | 
            +
                    mt = :concrete
         | 
| 318 | 
            +
                    mt = :cladding if specs[:clad] == :light
         | 
| 319 | 
            +
                    d  = 0.015
         | 
| 320 | 
            +
                    d  = 0.100     if specs[:clad] == :medium # e.g. terrace
         | 
| 321 | 
            +
                    d  = 0.200     if specs[:clad] == :heavy  # e.g. parking garage
         | 
| 322 | 
            +
                    a[:clad][:mat] = @@mats[mt]
         | 
| 323 | 
            +
                    a[:clad][:d  ] = d
         | 
| 324 | 
            +
                    a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 325 | 
            +
             | 
| 326 | 
            +
                    mt = :sheathing
         | 
| 327 | 
            +
                    d  = 0.015
         | 
| 328 | 
            +
                    a[:sheath][:mat] = @@mats[mt]
         | 
| 329 | 
            +
                    a[:sheath][:d  ] = d
         | 
| 330 | 
            +
                    a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 331 | 
            +
                  end
         | 
| 332 | 
            +
             | 
| 333 | 
            +
                  mt = :cellulose
         | 
| 334 | 
            +
                  mt = :polyiso   if specs[:frame] == :medium
         | 
| 335 | 
            +
                  mt = :mineral   if specs[:frame] == :heavy
         | 
| 336 | 
            +
                  d  = 0.100
         | 
| 337 | 
            +
                  a[:compo][:mat] = @@mats[mt]
         | 
| 338 | 
            +
                  a[:compo][:d  ] = d
         | 
| 339 | 
            +
                  a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 340 | 
            +
             | 
| 341 | 
            +
                  unless specs[:finish] == :none
         | 
| 342 | 
            +
                    mt = :concrete
         | 
| 343 | 
            +
                    mt = :drywall   if specs[:finish] == :light
         | 
| 344 | 
            +
                    d  = 0.015
         | 
| 345 | 
            +
                    d  = 0.100      if specs[:finish] == :medium # proxy for steel decking
         | 
| 346 | 
            +
                    d  = 0.200      if specs[:finish] == :heavy
         | 
| 347 | 
            +
                    a[:finish][:mat] = @@mats[mt]
         | 
| 348 | 
            +
                    a[:finish][:d  ] = d
         | 
| 349 | 
            +
                    a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 350 | 
            +
                  end
         | 
| 351 | 
            +
                when :floor # exposed
         | 
| 352 | 
            +
                  unless specs[:clad] == :none
         | 
| 353 | 
            +
                    mt = :cladding
         | 
| 354 | 
            +
                    d  = 0.015
         | 
| 355 | 
            +
                    a[:clad][:mat] = @@mats[mt]
         | 
| 356 | 
            +
                    a[:clad][:d  ] = d
         | 
| 357 | 
            +
                    a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 358 | 
            +
             | 
| 359 | 
            +
                    mt = :sheathing
         | 
| 360 | 
            +
                    d  = 0.015
         | 
| 361 | 
            +
                    a[:sheath][:mat] = @@mats[mt]
         | 
| 362 | 
            +
                    a[:sheath][:d  ] = d
         | 
| 363 | 
            +
                    a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 364 | 
            +
                  end
         | 
| 365 | 
            +
             | 
| 366 | 
            +
                  mt = :cellulose
         | 
| 367 | 
            +
                  mt = :polyiso   if specs[:frame] == :medium
         | 
| 368 | 
            +
                  mt = :mineral   if specs[:frame] == :heavy
         | 
| 369 | 
            +
                  d  = 0.100 # possibly an insulating layer to reset
         | 
| 370 | 
            +
                  a[:compo][:mat] = @@mats[mt]
         | 
| 371 | 
            +
                  a[:compo][:d  ] = d
         | 
| 372 | 
            +
                  a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 373 | 
            +
             | 
| 374 | 
            +
                  unless specs[:finish] == :none
         | 
| 375 | 
            +
                    mt = :concrete
         | 
| 376 | 
            +
                    mt = :sheathing if specs[:finish] == :light
         | 
| 377 | 
            +
                    d  = 0.015
         | 
| 378 | 
            +
                    d  = 0.100      if specs[:finish] == :medium
         | 
| 379 | 
            +
                    d  = 0.200      if specs[:finish] == :heavy
         | 
| 380 | 
            +
                    a[:finish][:mat] = @@mats[mt]
         | 
| 381 | 
            +
                    a[:finish][:d  ] = d
         | 
| 382 | 
            +
                    a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 383 | 
            +
                  end
         | 
| 384 | 
            +
                when :slab # basement slab or slab-on-grade
         | 
| 385 | 
            +
                  mt = :sand
         | 
| 386 | 
            +
                  d  = 0.100
         | 
| 387 | 
            +
                  a[:clad][:mat] = @@mats[mt]
         | 
| 388 | 
            +
                  a[:clad][:d  ] = d
         | 
| 389 | 
            +
                  a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 390 | 
            +
             | 
| 391 | 
            +
                  unless specs[:frame] == :none
         | 
| 392 | 
            +
                    mt = :polyiso
         | 
| 393 | 
            +
                    d  = 0.025
         | 
| 394 | 
            +
                    a[:sheath][:mat] = @@mats[mt]
         | 
| 395 | 
            +
                    a[:sheath][:d  ] = d
         | 
| 396 | 
            +
                    a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 397 | 
            +
                  end
         | 
| 398 | 
            +
             | 
| 399 | 
            +
                  mt = :concrete
         | 
| 400 | 
            +
                  d  = 0.100
         | 
| 401 | 
            +
                  d  = 0.200      if specs[:frame] == :heavy
         | 
| 402 | 
            +
                  a[:compo][:mat] = @@mats[mt]
         | 
| 403 | 
            +
                  a[:compo][:d  ] = d
         | 
| 404 | 
            +
                  a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 405 | 
            +
             | 
| 406 | 
            +
                  unless specs[:finish] == :none
         | 
| 407 | 
            +
                    mt = :sheathing
         | 
| 408 | 
            +
                    d  = 0.015
         | 
| 409 | 
            +
                    a[:finish][:mat] = @@mats[mt]
         | 
| 410 | 
            +
                    a[:finish][:d  ] = d
         | 
| 411 | 
            +
                    a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 412 | 
            +
                  end
         | 
| 413 | 
            +
                when :basement # wall
         | 
| 414 | 
            +
                  unless specs[:clad] == :none
         | 
| 415 | 
            +
                    mt = :concrete
         | 
| 416 | 
            +
                    mt = :sheathing if specs[:clad] == :light
         | 
| 417 | 
            +
                    d  = 0.100
         | 
| 418 | 
            +
                    d  = 0.015      if specs[:clad] == :light
         | 
| 419 | 
            +
                    a[:clad][:mat] = @@mats[mt]
         | 
| 420 | 
            +
                    a[:clad][:d  ] = d
         | 
| 421 | 
            +
                    a[:clad][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 422 | 
            +
             | 
| 423 | 
            +
                    mt = :polyiso
         | 
| 424 | 
            +
                    d  = 0.025
         | 
| 425 | 
            +
                    a[:sheath][:mat] = @@mats[mt]
         | 
| 426 | 
            +
                    a[:sheath][:d  ] = d
         | 
| 427 | 
            +
                    a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 428 | 
            +
             | 
| 429 | 
            +
                    mt = :concrete
         | 
| 430 | 
            +
                    d  = 0.200
         | 
| 431 | 
            +
                    a[:compo][:mat] = @@mats[mt]
         | 
| 432 | 
            +
                    a[:compo][:d  ] = d
         | 
| 433 | 
            +
                    a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 434 | 
            +
                  else
         | 
| 435 | 
            +
                    mt = :concrete
         | 
| 436 | 
            +
                    d  = 0.200
         | 
| 437 | 
            +
                    a[:sheath][:mat] = @@mats[mt]
         | 
| 438 | 
            +
                    a[:sheath][:d  ] = d
         | 
| 439 | 
            +
                    a[:sheath][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 440 | 
            +
             | 
| 441 | 
            +
                    unless specs[:finish] == :none
         | 
| 442 | 
            +
                      mt = :mineral
         | 
| 443 | 
            +
                      d  = 0.075
         | 
| 444 | 
            +
                      a[:compo][:mat] = @@mats[mt]
         | 
| 445 | 
            +
                      a[:compo][:d  ] = d
         | 
| 446 | 
            +
                      a[:compo][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 447 | 
            +
             | 
| 448 | 
            +
                      mt = :drywall
         | 
| 449 | 
            +
                      d  = 0.015
         | 
| 450 | 
            +
                      a[:finish][:mat] = @@mats[mt]
         | 
| 451 | 
            +
                      a[:finish][:d  ] = d
         | 
| 452 | 
            +
                      a[:finish][:id ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 453 | 
            +
                    end
         | 
| 454 | 
            +
                  end
         | 
| 455 | 
            +
                when :door # opaque
         | 
| 456 | 
            +
                  # 45mm insulated (composite) steel door.
         | 
| 457 | 
            +
                  mt = :door
         | 
| 458 | 
            +
                  d  = 0.045
         | 
| 459 | 
            +
             | 
| 460 | 
            +
                  a[:compo  ][:mat ] = @@mats[mt]
         | 
| 461 | 
            +
                  a[:compo  ][:d   ] = d
         | 
| 462 | 
            +
                  a[:compo  ][:id  ] = "OSut|#{mt}|#{format('%03d', d*1000)[-3..-1]}"
         | 
| 463 | 
            +
                when :window # e.g. patio doors (simple glazing)
         | 
| 464 | 
            +
                  # SimpleGlazingMaterial.
         | 
| 465 | 
            +
                  a[:glazing][:u   ]  = specs[:uo  ]
         | 
| 466 | 
            +
                  a[:glazing][:shgc]  = 0.450
         | 
| 467 | 
            +
                  a[:glazing][:shgc]  = specs[:shgc] if specs.key?(:shgc)
         | 
| 468 | 
            +
                  a[:glazing][:id  ]  = "OSut|window"
         | 
| 469 | 
            +
                  a[:glazing][:id  ] += "|U#{format('%.1f', a[:glazing][:u])}"
         | 
| 470 | 
            +
                  a[:glazing][:id  ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
         | 
| 471 | 
            +
                when :skylight
         | 
| 472 | 
            +
                  # SimpleGlazingMaterial.
         | 
| 473 | 
            +
                  a[:glazing][:u   ] = specs[:uo  ]
         | 
| 474 | 
            +
                  a[:glazing][:shgc] = 0.450
         | 
| 475 | 
            +
                  a[:glazing][:shgc] = specs[:shgc] if specs.key?(:shgc)
         | 
| 476 | 
            +
                  a[:glazing][:id  ]  = "OSut|skylight"
         | 
| 477 | 
            +
                  a[:glazing][:id  ] += "|U#{format('%.1f', a[:glazing][:u])}"
         | 
| 478 | 
            +
                  a[:glazing][:id  ] += "|SHGC#{format('%d', a[:glazing][:shgc]*100)}"
         | 
| 479 | 
            +
                end
         | 
| 480 | 
            +
             | 
| 481 | 
            +
                # Initiate layers.
         | 
| 482 | 
            +
                glazed = true
         | 
| 483 | 
            +
                glazed = false if a[:glazing].empty?
         | 
| 484 | 
            +
                layers = OpenStudio::Model::OpaqueMaterialVector.new   unless glazed
         | 
| 485 | 
            +
                layers = OpenStudio::Model::FenestrationMaterialVector.new if glazed
         | 
| 486 | 
            +
             | 
| 487 | 
            +
                if glazed
         | 
| 488 | 
            +
                  u    = a[:glazing][:u   ]
         | 
| 489 | 
            +
                  shgc = a[:glazing][:shgc]
         | 
| 490 | 
            +
                  lyr  = model.getSimpleGlazingByName(a[:glazing][:id])
         | 
| 491 | 
            +
             | 
| 492 | 
            +
                  if lyr.empty?
         | 
| 493 | 
            +
                    lyr = OpenStudio::Model::SimpleGlazing.new(model, u, shgc)
         | 
| 494 | 
            +
                    lyr.setName(a[:glazing][:id])
         | 
| 495 | 
            +
                  else
         | 
| 496 | 
            +
                    lyr = lyr.get
         | 
| 497 | 
            +
                  end
         | 
| 498 | 
            +
             | 
| 499 | 
            +
                  layers << lyr
         | 
| 500 | 
            +
                else
         | 
| 501 | 
            +
                  # Loop through each layer spec, and generate construction.
         | 
| 502 | 
            +
                  a.each do |i, l|
         | 
| 503 | 
            +
                    next if l.empty?
         | 
| 504 | 
            +
             | 
| 505 | 
            +
                    lyr = model.getStandardOpaqueMaterialByName(l[:id])
         | 
| 506 | 
            +
             | 
| 507 | 
            +
                    if lyr.empty?
         | 
| 508 | 
            +
                      lyr = OpenStudio::Model::StandardOpaqueMaterial.new(model)
         | 
| 509 | 
            +
                      lyr.setName(l[:id])
         | 
| 510 | 
            +
                      lyr.setThickness(l[:d])
         | 
| 511 | 
            +
                      lyr.setRoughness(         l[:mat][:rgh]) if l[:mat].key?(:rgh)
         | 
| 512 | 
            +
                      lyr.setConductivity(      l[:mat][:k  ]) if l[:mat].key?(:k  )
         | 
| 513 | 
            +
                      lyr.setDensity(           l[:mat][:rho]) if l[:mat].key?(:rho)
         | 
| 514 | 
            +
                      lyr.setSpecificHeat(      l[:mat][:cp ]) if l[:mat].key?(:cp )
         | 
| 515 | 
            +
                      lyr.setThermalAbsorptance(l[:mat][:thm]) if l[:mat].key?(:thm)
         | 
| 516 | 
            +
                      lyr.setSolarAbsorptance(  l[:mat][:sol]) if l[:mat].key?(:sol)
         | 
| 517 | 
            +
                      lyr.setVisibleAbsorptance(l[:mat][:vis]) if l[:mat].key?(:vis)
         | 
| 518 | 
            +
                    else
         | 
| 519 | 
            +
                      lyr = lyr.get
         | 
| 520 | 
            +
                    end
         | 
| 521 | 
            +
             | 
| 522 | 
            +
                    layers << lyr
         | 
| 523 | 
            +
                  end
         | 
| 524 | 
            +
                end
         | 
| 525 | 
            +
             | 
| 526 | 
            +
                c  = OpenStudio::Model::Construction.new(layers)
         | 
| 527 | 
            +
                c.setName(id)
         | 
| 528 | 
            +
             | 
| 529 | 
            +
                # Adjust insulating layer thickness or conductivity to match requested Uo.
         | 
| 530 | 
            +
                unless glazed
         | 
| 531 | 
            +
                  ro = 0
         | 
| 532 | 
            +
                  ro = 1 / specs[:uo] - @@film[ specs[:type] ] if specs[:uo] > 0
         | 
| 533 | 
            +
             | 
| 534 | 
            +
                  if specs[:type] == :door # 1x layer, adjust conductivity
         | 
| 535 | 
            +
                    layer = c.getLayer(0).to_StandardOpaqueMaterial
         | 
| 536 | 
            +
                    return invalid("#{id} standard material?", mth, 0) if layer.empty?
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                    layer = layer.get
         | 
| 539 | 
            +
                    k     = layer.thickness / ro
         | 
| 540 | 
            +
                    layer.setConductivity(k)
         | 
| 541 | 
            +
                  elsif ro > 0 # multiple layers, adjust insulating layer thickness
         | 
| 542 | 
            +
                    lyr = insulatingLayer(c)
         | 
| 543 | 
            +
                    return invalid("#{id} construction", mth, 0) if lyr[:index].nil?
         | 
| 544 | 
            +
                    return invalid("#{id} construction", mth, 0) if lyr[:type ].nil?
         | 
| 545 | 
            +
                    return invalid("#{id} construction", mth, 0) if lyr[:r    ].zero?
         | 
| 546 | 
            +
             | 
| 547 | 
            +
                    index = lyr[:index]
         | 
| 548 | 
            +
                    layer = c.getLayer(index).to_StandardOpaqueMaterial
         | 
| 549 | 
            +
                    return invalid("#{id} material @#{index}", mth, 0) if layer.empty?
         | 
| 550 | 
            +
             | 
| 551 | 
            +
                    layer = layer.get
         | 
| 552 | 
            +
                    k     = layer.conductivity
         | 
| 553 | 
            +
                    d     = (ro - rsi(c) + lyr[:r]) * k
         | 
| 554 | 
            +
                    return invalid("#{id} adjusted m", mth, 0) if d < 0.03
         | 
| 555 | 
            +
             | 
| 556 | 
            +
                    nom   = "OSut|"
         | 
| 557 | 
            +
                    nom  += layer.nameString.gsub(/[^a-z]/i, "").gsub("OSut", "")
         | 
| 558 | 
            +
                    nom  += "|"
         | 
| 559 | 
            +
                    nom  += format("%03d", d*1000)[-3..-1]
         | 
| 560 | 
            +
                    layer.setName(nom) if model.getStandardOpaqueMaterialByName(nom).empty?
         | 
| 561 | 
            +
                    layer.setThickness(d)
         | 
| 562 | 
            +
                  end
         | 
| 563 | 
            +
                end
         | 
| 564 | 
            +
             | 
| 565 | 
            +
                c
         | 
| 566 | 
            +
              end
         | 
| 567 | 
            +
             | 
| 568 | 
            +
              ##
         | 
| 569 | 
            +
              # Generates a solar shade (e.g. roller, textile) for glazed OpenStudio
         | 
| 570 | 
            +
              # SubSurfaces (v351+), controlled to minimize overheating in cooling months
         | 
| 571 | 
            +
              # (May to October in Northern Hemisphere), when outdoor dry bulb temperature
         | 
| 572 | 
            +
              # is above 18°C and impinging solar radiation is above 100 W/m2.
         | 
| 573 | 
            +
              #
         | 
| 574 | 
            +
              # @param subs [OpenStudio::Model::SubSurfaceVector] sub surfaces
         | 
| 575 | 
            +
              #
         | 
| 576 | 
            +
              # @return [Bool] whether successfully generated
         | 
| 577 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 578 | 
            +
              def genShade(subs = OpenStudio::Model::SubSurfaceVector.new)
         | 
| 579 | 
            +
                # Filter OpenStudio warnings for ShadingControl:
         | 
| 580 | 
            +
                #   ref: https://github.com/NREL/OpenStudio/issues/4911
         | 
| 581 | 
            +
                str = ".*(?<!ShadingControl)$"
         | 
| 582 | 
            +
                OpenStudio::Logger.instance.standardOutLogger.setChannelRegex(str)
         | 
| 583 | 
            +
             | 
| 584 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 585 | 
            +
                v   = OpenStudio.openStudioVersion.split(".").join.to_i
         | 
| 586 | 
            +
                cl  = OpenStudio::Model::SubSurfaceVector
         | 
| 587 | 
            +
                return mismatch("subs ", subs,  cl2, mth, DBG, false) unless subs.is_a?(cl)
         | 
| 588 | 
            +
                return empty(   "subs",              mth, WRN, false)     if subs.empty?
         | 
| 589 | 
            +
                return false                                              if v < 321
         | 
| 590 | 
            +
             | 
| 591 | 
            +
                # Shading availability period.
         | 
| 592 | 
            +
                mdl   = subs.first.model
         | 
| 593 | 
            +
                id    = "onoff"
         | 
| 594 | 
            +
                onoff = mdl.getScheduleTypeLimitsByName(id)
         | 
| 595 | 
            +
             | 
| 596 | 
            +
                if onoff.empty?
         | 
| 597 | 
            +
                  onoff = OpenStudio::Model::ScheduleTypeLimits.new(mdl)
         | 
| 598 | 
            +
                  onoff.setName(id)
         | 
| 599 | 
            +
                  onoff.setLowerLimitValue(0)
         | 
| 600 | 
            +
                  onoff.setUpperLimitValue(1)
         | 
| 601 | 
            +
                  onoff.setNumericType("Discrete")
         | 
| 602 | 
            +
                  onoff.setUnitType("Availability")
         | 
| 603 | 
            +
                else
         | 
| 604 | 
            +
                  onoff = onoff.get
         | 
| 605 | 
            +
                end
         | 
| 606 | 
            +
             | 
| 607 | 
            +
                # Shading schedule.
         | 
| 608 | 
            +
                id  = "OSut|SHADE|Ruleset"
         | 
| 609 | 
            +
                sch = mdl.getScheduleRulesetByName(id)
         | 
| 610 | 
            +
             | 
| 611 | 
            +
                if sch.empty?
         | 
| 612 | 
            +
                  sch = OpenStudio::Model::ScheduleRuleset.new(mdl, 0)
         | 
| 613 | 
            +
                  sch.setName(id)
         | 
| 614 | 
            +
                  sch.setScheduleTypeLimits(onoff)
         | 
| 615 | 
            +
                  sch.defaultDaySchedule.setName("OSut|Shade|Ruleset|Default")
         | 
| 616 | 
            +
                else
         | 
| 617 | 
            +
                  sch = sch.get
         | 
| 618 | 
            +
                end
         | 
| 619 | 
            +
             | 
| 620 | 
            +
                # Summer cooling rule.
         | 
| 621 | 
            +
                id   = "OSut|SHADE|ScheduleRule"
         | 
| 622 | 
            +
                rule = mdl.getScheduleRuleByName(id)
         | 
| 623 | 
            +
             | 
| 624 | 
            +
                if rule.empty?
         | 
| 625 | 
            +
                  may     = OpenStudio::MonthOfYear.new("May")
         | 
| 626 | 
            +
                  october = OpenStudio::MonthOfYear.new("Oct")
         | 
| 627 | 
            +
                  start   = OpenStudio::Date.new(may, 1)
         | 
| 628 | 
            +
                  finish  = OpenStudio::Date.new(october, 31)
         | 
| 629 | 
            +
             | 
| 630 | 
            +
                  rule = OpenStudio::Model::ScheduleRule.new(sch)
         | 
| 631 | 
            +
                  rule.setName(id)
         | 
| 632 | 
            +
                  rule.setStartDate(start)
         | 
| 633 | 
            +
                  rule.setEndDate(finish)
         | 
| 634 | 
            +
                  rule.setApplyAllDays(true)
         | 
| 635 | 
            +
                  rule.daySchedule.setName("OSut|Shade|Rule|Default")
         | 
| 636 | 
            +
                  rule.daySchedule.addValue(OpenStudio::Time.new(0,24,0,0), 1)
         | 
| 637 | 
            +
                else
         | 
| 638 | 
            +
                  rule = rule.get
         | 
| 639 | 
            +
                end
         | 
| 640 | 
            +
             | 
| 641 | 
            +
                # Shade object.
         | 
| 642 | 
            +
                id  = "OSut|Shade"
         | 
| 643 | 
            +
                shd = mdl.getShadeByName(id)
         | 
| 644 | 
            +
             | 
| 645 | 
            +
                if shd.empty?
         | 
| 646 | 
            +
                  shd = OpenStudio::Model::Shade.new(mdl)
         | 
| 647 | 
            +
                  shd.setName(id)
         | 
| 648 | 
            +
                else
         | 
| 649 | 
            +
                  shd = shd.get
         | 
| 650 | 
            +
                end
         | 
| 651 | 
            +
             | 
| 652 | 
            +
                # Shading control (unique to each call).
         | 
| 653 | 
            +
                id  = "OSut|ShadingControl"
         | 
| 654 | 
            +
                ctl = OpenStudio::Model::ShadingControl.new(shd)
         | 
| 655 | 
            +
                ctl.setName(id)
         | 
| 656 | 
            +
                ctl.setSchedule(sch)
         | 
| 657 | 
            +
                ctl.setShadingControlType("OnIfHighOutdoorAirTempAndHighSolarOnWindow")
         | 
| 658 | 
            +
                ctl.setSetpoint(18)   # °C
         | 
| 659 | 
            +
                ctl.setSetpoint2(100) # W/m2
         | 
| 660 | 
            +
                ctl.setMultipleSurfaceControlType("Group")
         | 
| 661 | 
            +
                ctl.setSubSurfaces(subs)
         | 
| 662 | 
            +
              end
         | 
| 663 | 
            +
             | 
| 664 | 
            +
              ##
         | 
| 665 | 
            +
              # Generates an internal mass definition and instances for target spaces.
         | 
| 666 | 
            +
              #
         | 
| 667 | 
            +
              # @param sps [OpenStudio::Model::SpaceVector] target spaces
         | 
| 668 | 
            +
              # @param ratio [Numeric] internal mass surface / floor areas
         | 
| 669 | 
            +
              #
         | 
| 670 | 
            +
              # @return [Bool] whether successfully generated
         | 
| 671 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 672 | 
            +
              def genMass(sps = OpenStudio::Model::SpaceVector.new, ratio = 2.0)
         | 
| 673 | 
            +
                # This is largely adapted from OpenStudio-Standards:
         | 
| 674 | 
            +
                #
         | 
| 675 | 
            +
                #   https://github.com/NREL/openstudio-standards/blob/
         | 
| 676 | 
            +
                #   d332605c2f7a35039bf658bf55cad40a7bcac317/lib/openstudio-standards/
         | 
| 677 | 
            +
                #   prototypes/common/objects/Prototype.Model.rb#L786
         | 
| 678 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 679 | 
            +
                cl1 = OpenStudio::Model::SpaceVector
         | 
| 680 | 
            +
                cl2 = Numeric
         | 
| 681 | 
            +
                no  = false
         | 
| 682 | 
            +
                return mismatch("spaces",   sps, cl1, mth, DBG, no) unless sps.is_a?(cl1)
         | 
| 683 | 
            +
                return mismatch( "ratio", ratio, cl2, mth, DBG, no) unless ratio.is_a?(cl2)
         | 
| 684 | 
            +
                return empty(   "spaces",             mth, WRN, no)     if sps.empty?
         | 
| 685 | 
            +
                return negative( "ratio",             mth, ERR, no)     if ratio < 0
         | 
| 686 | 
            +
             | 
| 687 | 
            +
                # A single material.
         | 
| 688 | 
            +
                mdl = sps.first.model
         | 
| 689 | 
            +
                id  = "OSut|MASS|Material"
         | 
| 690 | 
            +
                mat = mdl.getOpaqueMaterialByName(id)
         | 
| 691 | 
            +
             | 
| 692 | 
            +
                if mat.empty?
         | 
| 693 | 
            +
                  mat = OpenStudio::Model::StandardOpaqueMaterial.new(mdl)
         | 
| 694 | 
            +
                  mat.setName(id)
         | 
| 695 | 
            +
                  mat.setRoughness("MediumRough")
         | 
| 696 | 
            +
                  mat.setThickness(0.15)
         | 
| 697 | 
            +
                  mat.setConductivity(1.12)
         | 
| 698 | 
            +
                  mat.setDensity(540)
         | 
| 699 | 
            +
                  mat.setSpecificHeat(1210)
         | 
| 700 | 
            +
                  mat.setThermalAbsorptance(0.9)
         | 
| 701 | 
            +
                  mat.setSolarAbsorptance(0.7)
         | 
| 702 | 
            +
                  mat.setVisibleAbsorptance(0.17)
         | 
| 703 | 
            +
                else
         | 
| 704 | 
            +
                  mat = mat.get
         | 
| 705 | 
            +
                end
         | 
| 706 | 
            +
             | 
| 707 | 
            +
                # A single, 1x layered construction.
         | 
| 708 | 
            +
                id  = "OSut|MASS|Construction"
         | 
| 709 | 
            +
                con = mdl.getConstructionByName(id)
         | 
| 710 | 
            +
             | 
| 711 | 
            +
                if con.empty?
         | 
| 712 | 
            +
                  con = OpenStudio::Model::Construction.new(mdl)
         | 
| 713 | 
            +
                  con.setName(id)
         | 
| 714 | 
            +
                  layers = OpenStudio::Model::MaterialVector.new
         | 
| 715 | 
            +
                  layers << mat
         | 
| 716 | 
            +
                  con.setLayers(layers)
         | 
| 717 | 
            +
                else
         | 
| 718 | 
            +
                  con = con.get
         | 
| 719 | 
            +
                end
         | 
| 720 | 
            +
             | 
| 721 | 
            +
                id = "OSut|InternalMassDefinition|" + (format "%.2f", ratio)
         | 
| 722 | 
            +
                df = mdl.getInternalMassDefinitionByName(id)
         | 
| 723 | 
            +
             | 
| 724 | 
            +
                if df.empty?
         | 
| 725 | 
            +
                  df = OpenStudio::Model::InternalMassDefinition.new(mdl)
         | 
| 726 | 
            +
                  df.setName(id)
         | 
| 727 | 
            +
                  df.setConstruction(con)
         | 
| 728 | 
            +
                  df.setSurfaceAreaperSpaceFloorArea(ratio)
         | 
| 729 | 
            +
                else
         | 
| 730 | 
            +
                  df = df.get
         | 
| 731 | 
            +
                end
         | 
| 732 | 
            +
             | 
| 733 | 
            +
                sps.each do |sp|
         | 
| 734 | 
            +
                  mass = OpenStudio::Model::InternalMass.new(df)
         | 
| 735 | 
            +
                  mass.setName("OSut|InternalMass|#{sp.nameString}")
         | 
| 736 | 
            +
                  mass.setSpace(sp)
         | 
| 737 | 
            +
                end
         | 
| 738 | 
            +
             | 
| 739 | 
            +
                true
         | 
| 740 | 
            +
              end
         | 
| 741 | 
            +
             | 
| 742 | 
            +
              ##
         | 
| 743 | 
            +
              # Validates if a default construction set holds a base construction.
         | 
| 744 | 
            +
              #
         | 
| 745 | 
            +
              # @param set [OpenStudio::Model::DefaultConstructionSet] a default set
         | 
| 746 | 
            +
              # @param bse [OpensStudio::Model::ConstructionBase] a construction base
         | 
| 747 | 
            +
              # @param gr [Bool] if ground-facing surface
         | 
| 748 | 
            +
              # @param ex [Bool] if exterior-facing surface
         | 
| 749 | 
            +
              # @param tp [#to_s] a surface type
         | 
| 750 | 
            +
              #
         | 
| 751 | 
            +
              # @return [Bool] whether default set holds construction
         | 
| 752 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 753 | 
            +
              def holdsConstruction?(set = nil, bse = nil, gr = false, ex = false, tp = "")
         | 
| 754 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 755 | 
            +
                cl1 = OpenStudio::Model::DefaultConstructionSet
         | 
| 756 | 
            +
                cl2 = OpenStudio::Model::ConstructionBase
         | 
| 757 | 
            +
                ck1 = set.respond_to?(NS)
         | 
| 758 | 
            +
                ck2 = bse.respond_to?(NS)
         | 
| 759 | 
            +
                return invalid("set" , mth, 1, DBG, false) unless ck1
         | 
| 760 | 
            +
                return invalid("base", mth, 2, DBG, false) unless ck2
         | 
| 761 | 
            +
             | 
| 762 | 
            +
                id1 = set.nameString
         | 
| 763 | 
            +
                id2 = bse.nameString
         | 
| 764 | 
            +
                ck1 = set.is_a?(cl1)
         | 
| 765 | 
            +
                ck2 = bse.is_a?(cl2)
         | 
| 766 | 
            +
                ck3 = [true, false].include?(gr)
         | 
| 767 | 
            +
                ck4 = [true, false].include?(ex)
         | 
| 768 | 
            +
                ck5 = tp.respond_to?(:to_s)
         | 
| 769 | 
            +
                return mismatch(id1, set, cl1, mth,    DBG, false) unless ck1
         | 
| 770 | 
            +
                return mismatch(id2, bse, cl2, mth,    DBG, false) unless ck2
         | 
| 771 | 
            +
                return invalid("ground"      , mth, 3, DBG, false) unless ck3
         | 
| 772 | 
            +
                return invalid("exterior"    , mth, 4, DBG, false) unless ck4
         | 
| 773 | 
            +
                return invalid("surface type", mth, 5, DBG, false) unless ck5
         | 
| 774 | 
            +
             | 
| 775 | 
            +
                type = trim(tp).downcase
         | 
| 776 | 
            +
                ck1  = ["floor", "wall", "roofceiling"].include?(type)
         | 
| 777 | 
            +
                return invalid("surface type", mth, 5, DBG, false) unless ck1
         | 
| 778 | 
            +
             | 
| 779 | 
            +
                constructions = nil
         | 
| 780 | 
            +
             | 
| 781 | 
            +
                if gr
         | 
| 782 | 
            +
                  unless set.defaultGroundContactSurfaceConstructions.empty?
         | 
| 783 | 
            +
                    constructions = set.defaultGroundContactSurfaceConstructions.get
         | 
| 784 | 
            +
                  end
         | 
| 785 | 
            +
                elsif ex
         | 
| 786 | 
            +
                  unless set.defaultExteriorSurfaceConstructions.empty?
         | 
| 787 | 
            +
                    constructions = set.defaultExteriorSurfaceConstructions.get
         | 
| 788 | 
            +
                  end
         | 
| 789 | 
            +
                else
         | 
| 790 | 
            +
                  unless set.defaultInteriorSurfaceConstructions.empty?
         | 
| 791 | 
            +
                    constructions = set.defaultInteriorSurfaceConstructions.get
         | 
| 792 | 
            +
                  end
         | 
| 793 | 
            +
                end
         | 
| 794 | 
            +
             | 
| 795 | 
            +
                return false unless constructions
         | 
| 796 | 
            +
             | 
| 797 | 
            +
                case type
         | 
| 798 | 
            +
                when "roofceiling"
         | 
| 799 | 
            +
                  unless constructions.roofCeilingConstruction.empty?
         | 
| 800 | 
            +
                    construction = constructions.roofCeilingConstruction.get
         | 
| 801 | 
            +
                    return true if construction == bse
         | 
| 802 | 
            +
                  end
         | 
| 803 | 
            +
                when "floor"
         | 
| 804 | 
            +
                  unless constructions.floorConstruction.empty?
         | 
| 805 | 
            +
                    construction = constructions.floorConstruction.get
         | 
| 806 | 
            +
                    return true if construction == bse
         | 
| 807 | 
            +
                  end
         | 
| 808 | 
            +
                else
         | 
| 809 | 
            +
                  unless constructions.wallConstruction.empty?
         | 
| 810 | 
            +
                    construction = constructions.wallConstruction.get
         | 
| 811 | 
            +
                    return true if construction == bse
         | 
| 812 | 
            +
                  end
         | 
| 813 | 
            +
                end
         | 
| 814 | 
            +
             | 
| 815 | 
            +
                false
         | 
| 816 | 
            +
              end
         | 
| 817 | 
            +
             | 
| 818 | 
            +
              ##
         | 
| 819 | 
            +
              # Returns a surface's default construction set.
         | 
| 820 | 
            +
              #
         | 
| 821 | 
            +
              # @param s [OpenStudio::Model::Surface] a surface
         | 
| 822 | 
            +
              #
         | 
| 823 | 
            +
              # @return [OpenStudio::Model::DefaultConstructionSet] default set
         | 
| 824 | 
            +
              # @return [nil] if invalid input (see logs)
         | 
| 825 | 
            +
              def defaultConstructionSet(s = nil)
         | 
| 826 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 827 | 
            +
                cl  = OpenStudio::Model::Surface
         | 
| 828 | 
            +
                return invalid("surface", mth, 1) unless s.respond_to?(NS)
         | 
| 829 | 
            +
             | 
| 830 | 
            +
                id = s.nameString
         | 
| 831 | 
            +
                ok = s.isConstructionDefaulted
         | 
| 832 | 
            +
                m1 = "#{id} construction not defaulted (#{mth})"
         | 
| 833 | 
            +
                m2 = "#{id} construction"
         | 
| 834 | 
            +
                m3 = "#{id} space"
         | 
| 835 | 
            +
                return mismatch(id, s, cl, mth) unless s.is_a?(cl)
         | 
| 836 | 
            +
             | 
| 837 | 
            +
                log(ERR, m1)           unless ok
         | 
| 838 | 
            +
                return nil             unless ok
         | 
| 839 | 
            +
                return empty(m2, mth, ERR) if s.construction.empty?
         | 
| 840 | 
            +
                return empty(m3, mth, ERR) if s.space.empty?
         | 
| 841 | 
            +
             | 
| 842 | 
            +
                mdl      = s.model
         | 
| 843 | 
            +
                base     = s.construction.get
         | 
| 844 | 
            +
                space    = s.space.get
         | 
| 845 | 
            +
                type     = s.surfaceType
         | 
| 846 | 
            +
                ground   = false
         | 
| 847 | 
            +
                exterior = false
         | 
| 848 | 
            +
             | 
| 849 | 
            +
                if s.isGroundSurface
         | 
| 850 | 
            +
                  ground = true
         | 
| 851 | 
            +
                elsif s.outsideBoundaryCondition.downcase == "outdoors"
         | 
| 852 | 
            +
                  exterior = true
         | 
| 853 | 
            +
                end
         | 
| 854 | 
            +
             | 
| 855 | 
            +
                unless space.defaultConstructionSet.empty?
         | 
| 856 | 
            +
                  set = space.defaultConstructionSet.get
         | 
| 857 | 
            +
                  return set if holdsConstruction?(set, base, ground, exterior, type)
         | 
| 858 | 
            +
                end
         | 
| 859 | 
            +
             | 
| 860 | 
            +
                unless space.spaceType.empty?
         | 
| 861 | 
            +
                  spacetype = space.spaceType.get
         | 
| 862 | 
            +
             | 
| 863 | 
            +
                  unless spacetype.defaultConstructionSet.empty?
         | 
| 864 | 
            +
                    set = spacetype.defaultConstructionSet.get
         | 
| 865 | 
            +
                    return set if holdsConstruction?(set, base, ground, exterior, type)
         | 
| 866 | 
            +
                  end
         | 
| 867 | 
            +
                end
         | 
| 868 | 
            +
             | 
| 869 | 
            +
                unless space.buildingStory.empty?
         | 
| 870 | 
            +
                  story = space.buildingStory.get
         | 
| 871 | 
            +
             | 
| 872 | 
            +
                  unless story.defaultConstructionSet.empty?
         | 
| 873 | 
            +
                    set = story.defaultConstructionSet.get
         | 
| 874 | 
            +
                    return set if holdsConstruction?(set, base, ground, exterior, type)
         | 
| 875 | 
            +
                  end
         | 
| 876 | 
            +
                end
         | 
| 877 | 
            +
             | 
| 878 | 
            +
                building = mdl.getBuilding
         | 
| 879 | 
            +
             | 
| 880 | 
            +
                unless building.defaultConstructionSet.empty?
         | 
| 881 | 
            +
                  set = building.defaultConstructionSet.get
         | 
| 882 | 
            +
                  return set if holdsConstruction?(set, base, ground, exterior, type)
         | 
| 883 | 
            +
                end
         | 
| 884 | 
            +
             | 
| 885 | 
            +
                nil
         | 
| 886 | 
            +
              end
         | 
| 887 | 
            +
             | 
| 888 | 
            +
              ##
         | 
| 889 | 
            +
              # Validates if every material in a layered construction is standard & opaque.
         | 
| 890 | 
            +
              #
         | 
| 891 | 
            +
              # @param lc [OpenStudio::LayeredConstruction] a layered construction
         | 
| 892 | 
            +
              #
         | 
| 893 | 
            +
              # @return [Bool] whether all layers are valid
         | 
| 894 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 895 | 
            +
              def standardOpaqueLayers?(lc = nil)
         | 
| 896 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 897 | 
            +
                cl  = OpenStudio::Model::LayeredConstruction
         | 
| 898 | 
            +
                return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
         | 
| 899 | 
            +
                return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
         | 
| 900 | 
            +
             | 
| 901 | 
            +
                lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
         | 
| 902 | 
            +
             | 
| 903 | 
            +
                true
         | 
| 904 | 
            +
              end
         | 
| 905 | 
            +
             | 
| 906 | 
            +
              ##
         | 
| 907 | 
            +
              # Returns total (standard opaque) layered construction thickness (m).
         | 
| 908 | 
            +
              #
         | 
| 909 | 
            +
              # @param lc [OpenStudio::LayeredConstruction] a layered construction
         | 
| 910 | 
            +
              #
         | 
| 911 | 
            +
              # @return [Float] construction thickness
         | 
| 912 | 
            +
              # @return [0.0] if invalid input (see logs)
         | 
| 913 | 
            +
              def thickness(lc = nil)
         | 
| 914 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 915 | 
            +
                cl  = OpenStudio::Model::LayeredConstruction
         | 
| 916 | 
            +
                return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
         | 
| 917 | 
            +
             | 
| 918 | 
            +
                id = lc.nameString
         | 
| 919 | 
            +
                return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl)
         | 
| 920 | 
            +
             | 
| 921 | 
            +
                ok = standardOpaqueLayers?(lc)
         | 
| 922 | 
            +
                log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})")  unless ok
         | 
| 923 | 
            +
                return 0.0                                                        unless ok
         | 
| 924 | 
            +
             | 
| 925 | 
            +
                thickness = 0.0
         | 
| 926 | 
            +
                lc.layers.each { |m| thickness += m.thickness }
         | 
| 927 | 
            +
             | 
| 928 | 
            +
                thickness
         | 
| 929 | 
            +
              end
         | 
| 930 | 
            +
             | 
| 931 | 
            +
              ##
         | 
| 932 | 
            +
              # Returns total air film resistance of a fenestrated construction (m2•K/W)
         | 
| 933 | 
            +
              #
         | 
| 934 | 
            +
              # @param usi [Numeric] a fenestrated construction's U-factor (W/m2•K)
         | 
| 935 | 
            +
              #
         | 
| 936 | 
            +
              # @return [Float] total air film resistances
         | 
| 937 | 
            +
              # @return [0.1216] if invalid input (see logs)
         | 
| 938 | 
            +
              def glazingAirFilmRSi(usi = 5.85)
         | 
| 939 | 
            +
                # The sum of thermal resistances of calculated exterior and interior film
         | 
| 940 | 
            +
                # coefficients under standard winter conditions are taken from:
         | 
| 941 | 
            +
                #
         | 
| 942 | 
            +
                #   https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
         | 
| 943 | 
            +
                #   window-calculation-module.html#simple-window-model
         | 
| 944 | 
            +
                #
         | 
| 945 | 
            +
                # These remain acceptable approximations for flat windows, yet likely
         | 
| 946 | 
            +
                # unsuitable for subsurfaces with curved or projecting shapes like domed
         | 
| 947 | 
            +
                # skylights. The solution here is considered an adequate fix for reporting,
         | 
| 948 | 
            +
                # awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
         | 
| 949 | 
            +
                # (or ISO) air film resistances under standard winter conditions.
         | 
| 950 | 
            +
                #
         | 
| 951 | 
            +
                # For U-factors above 8.0 W/m2•K (or invalid input), the function returns
         | 
| 952 | 
            +
                # 0.1216 m2•K/W, which corresponds to a construction with a single glass
         | 
| 953 | 
            +
                # layer thickness of 2mm & k = ~0.6 W/m.K.
         | 
| 954 | 
            +
                #
         | 
| 955 | 
            +
                # The EnergyPlus Engineering calculations were designed for vertical
         | 
| 956 | 
            +
                # windows - not horizontal, slanted or domed surfaces - use with caution.
         | 
| 957 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 958 | 
            +
                cl  = Numeric
         | 
| 959 | 
            +
                return mismatch("usi", usi, cl, mth,    DBG, 0.1216)  unless usi.is_a?(cl)
         | 
| 960 | 
            +
                return invalid("usi",           mth, 1, WRN, 0.1216)      if usi > 8.0
         | 
| 961 | 
            +
                return negative("usi",          mth,    WRN, 0.1216)      if usi < 0
         | 
| 962 | 
            +
                return zero("usi",              mth,    WRN, 0.1216)      if usi.abs < TOL
         | 
| 963 | 
            +
             | 
| 964 | 
            +
                rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film
         | 
| 965 | 
            +
                return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
         | 
| 966 | 
            +
                return rsi + 1 / (1.788041 * usi - 2.886625)
         | 
| 967 | 
            +
              end
         | 
| 968 | 
            +
             | 
| 969 | 
            +
              ##
         | 
| 970 | 
            +
              # Returns a construction's 'standard calc' thermal resistance (m2•K/W), which
         | 
| 971 | 
            +
              # includes air film resistances. It excludes insulating effects of shades,
         | 
| 972 | 
            +
              # screens, etc. in the case of fenestrated constructions.
         | 
| 973 | 
            +
              #
         | 
| 974 | 
            +
              # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
         | 
| 975 | 
            +
              # @param film [Numeric] thermal resistance of surface air films (m2•K/W)
         | 
| 976 | 
            +
              # @param t [Numeric] gas temperature (°C) (optional)
         | 
| 977 | 
            +
              #
         | 
| 978 | 
            +
              # @return [Float] layered construction's thermal resistance
         | 
| 979 | 
            +
              # @return [0.0] if invalid input (see logs)
         | 
| 980 | 
            +
              def rsi(lc = nil, film = 0.0, t = 0.0)
         | 
| 981 | 
            +
                # This is adapted from BTAP's Material Module "get_conductance" (P. Lopez)
         | 
| 982 | 
            +
                #
         | 
| 983 | 
            +
                #   https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
         | 
| 984 | 
            +
                #   c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
         | 
| 985 | 
            +
                #   btap_equest_converter/envelope.rb#L122
         | 
| 986 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 987 | 
            +
                cl1 = OpenStudio::Model::LayeredConstruction
         | 
| 988 | 
            +
                cl2 = Numeric
         | 
| 989 | 
            +
                return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS)
         | 
| 990 | 
            +
             | 
| 991 | 
            +
                id = lc.nameString
         | 
| 992 | 
            +
                return mismatch(id,       lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1)
         | 
| 993 | 
            +
                return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2)
         | 
| 994 | 
            +
                return mismatch("temp K",  t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2)
         | 
| 995 | 
            +
             | 
| 996 | 
            +
                t += 273.0 # °C to K
         | 
| 997 | 
            +
                return negative("temp K", mth, ERR, 0.0) if t < 0
         | 
| 998 | 
            +
                return negative("film",   mth, ERR, 0.0) if film < 0
         | 
| 999 | 
            +
             | 
| 1000 | 
            +
                rsi = film
         | 
| 1001 | 
            +
             | 
| 1002 | 
            +
                lc.layers.each do |m|
         | 
| 1003 | 
            +
                  # Fenestration materials first.
         | 
| 1004 | 
            +
                  empty = m.to_SimpleGlazing.empty?
         | 
| 1005 | 
            +
                  return 1 / m.to_SimpleGlazing.get.uFactor                     unless empty
         | 
| 1006 | 
            +
             | 
| 1007 | 
            +
                  empty = m.to_StandardGlazing.empty?
         | 
| 1008 | 
            +
                  rsi += m.to_StandardGlazing.get.thermalResistance             unless empty
         | 
| 1009 | 
            +
                  empty = m.to_RefractionExtinctionGlazing.empty?
         | 
| 1010 | 
            +
                  rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
         | 
| 1011 | 
            +
                  empty = m.to_Gas.empty?
         | 
| 1012 | 
            +
                  rsi += m.to_Gas.get.getThermalResistance(t)                   unless empty
         | 
| 1013 | 
            +
                  empty = m.to_GasMixture.empty?
         | 
| 1014 | 
            +
                  rsi += m.to_GasMixture.get.getThermalResistance(t)            unless empty
         | 
| 1015 | 
            +
             | 
| 1016 | 
            +
                  # Opaque materials next.
         | 
| 1017 | 
            +
                  empty = m.to_StandardOpaqueMaterial.empty?
         | 
| 1018 | 
            +
                  rsi += m.to_StandardOpaqueMaterial.get.thermalResistance      unless empty
         | 
| 1019 | 
            +
                  empty = m.to_MasslessOpaqueMaterial.empty?
         | 
| 1020 | 
            +
                  rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance      unless empty
         | 
| 1021 | 
            +
                  empty = m.to_RoofVegetation.empty?
         | 
| 1022 | 
            +
                  rsi += m.to_RoofVegetation.get.thermalResistance              unless empty
         | 
| 1023 | 
            +
                  empty = m.to_AirGap.empty?
         | 
| 1024 | 
            +
                  rsi += m.to_AirGap.get.thermalResistance                      unless empty
         | 
| 1025 | 
            +
                end
         | 
| 1026 | 
            +
             | 
| 1027 | 
            +
                rsi
         | 
| 1028 | 
            +
              end
         | 
| 1029 | 
            +
             | 
| 1030 | 
            +
              ##
         | 
| 1031 | 
            +
              # Identifies a layered construction's (opaque) insulating layer. The method
         | 
| 1032 | 
            +
              # returns a 3-keyed hash :index, the insulating layer index [0, n layers)
         | 
| 1033 | 
            +
              # within the layered construction; :type, either :standard or :massless; and
         | 
| 1034 | 
            +
              # :r, material thermal resistance in m2•K/W.
         | 
| 1035 | 
            +
              #
         | 
| 1036 | 
            +
              # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
         | 
| 1037 | 
            +
              #
         | 
| 1038 | 
            +
              # @return [Hash] index: (Integer), type: (Symbol), r: (Float)
         | 
| 1039 | 
            +
              # @return [Hash] index: nil, type: nil, r: 0 if invalid input (see logs)
         | 
| 1040 | 
            +
              def insulatingLayer(lc = nil)
         | 
| 1041 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 1042 | 
            +
                cl  = OpenStudio::Model::LayeredConstruction
         | 
| 1043 | 
            +
                res = { index: nil, type: nil, r: 0.0 }
         | 
| 1044 | 
            +
                i   = 0  # iterator
         | 
| 1045 | 
            +
                return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
         | 
| 1046 | 
            +
             | 
| 1047 | 
            +
                id   = lc.nameString
         | 
| 1048 | 
            +
                return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
         | 
| 1049 | 
            +
             | 
| 1050 | 
            +
                lc.layers.each do |m|
         | 
| 1051 | 
            +
                  unless m.to_MasslessOpaqueMaterial.empty?
         | 
| 1052 | 
            +
                    m             = m.to_MasslessOpaqueMaterial.get
         | 
| 1053 | 
            +
             | 
| 1054 | 
            +
                    if m.thermalResistance < 0.001 || m.thermalResistance < res[:r]
         | 
| 1055 | 
            +
                      i += 1
         | 
| 1056 | 
            +
                      next
         | 
| 1057 | 
            +
                    else
         | 
| 1058 | 
            +
                      res[:r    ] = m.thermalResistance
         | 
| 1059 | 
            +
                      res[:index] = i
         | 
| 1060 | 
            +
                      res[:type ] = :massless
         | 
| 1061 | 
            +
                    end
         | 
| 1062 | 
            +
                  end
         | 
| 1063 | 
            +
             | 
| 1064 | 
            +
                  unless m.to_StandardOpaqueMaterial.empty?
         | 
| 1065 | 
            +
                    m             = m.to_StandardOpaqueMaterial.get
         | 
| 1066 | 
            +
                    k             = m.thermalConductivity
         | 
| 1067 | 
            +
                    d             = m.thickness
         | 
| 1068 | 
            +
             | 
| 1069 | 
            +
                    if d < 0.003 || k > 3.0 || d / k < res[:r]
         | 
| 1070 | 
            +
                      i += 1
         | 
| 1071 | 
            +
                      next
         | 
| 1072 | 
            +
                    else
         | 
| 1073 | 
            +
                      res[:r    ] = d / k
         | 
| 1074 | 
            +
                      res[:index] = i
         | 
| 1075 | 
            +
                      res[:type ] = :standard
         | 
| 1076 | 
            +
                    end
         | 
| 1077 | 
            +
                  end
         | 
| 1078 | 
            +
             | 
| 1079 | 
            +
                  i += 1
         | 
| 1080 | 
            +
                end
         | 
| 1081 | 
            +
             | 
| 1082 | 
            +
                res
         | 
| 1083 | 
            +
              end
         | 
| 1084 | 
            +
             | 
| 1085 | 
            +
              ##
         | 
| 1086 | 
            +
              # Validates whether opaque surface can be considered as a curtain wall (or
         | 
| 1087 | 
            +
              # similar technology) spandrel, regardless of construction layers, by looking
         | 
| 1088 | 
            +
              # up AdditionalProperties or its identifier.
         | 
| 1089 | 
            +
              #
         | 
| 1090 | 
            +
              # @param s [OpenStudio::Model::Surface] an opaque surface
         | 
| 1091 | 
            +
              #
         | 
| 1092 | 
            +
              # @return [Bool] whether surface can be considered 'spandrel'
         | 
| 1093 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 1094 | 
            +
              def spandrel?(s = nil)
         | 
| 1095 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 1096 | 
            +
                cl  = OpenStudio::Model::Surface
         | 
| 1097 | 
            +
                return invalid("surface", mth, 1, DBG, false) unless s.respond_to?(NS)
         | 
| 1098 | 
            +
             | 
| 1099 | 
            +
                id = s.nameString
         | 
| 1100 | 
            +
                m1  = "#{id}:spandrel"
         | 
| 1101 | 
            +
                m2  = "#{id}:spandrel:boolean"
         | 
| 1102 | 
            +
             | 
| 1103 | 
            +
                if s.additionalProperties.hasFeature("spandrel")
         | 
| 1104 | 
            +
                  val = s.additionalProperties.getFeatureAsBoolean("spandrel")
         | 
| 1105 | 
            +
                  return invalid(m1, mth, 1, ERR, false) if val.empty?
         | 
| 1106 | 
            +
             | 
| 1107 | 
            +
                  val = val.get
         | 
| 1108 | 
            +
                  return invalid(m2, mth, 1, ERR, false) unless [true, false].include?(val)
         | 
| 1109 | 
            +
                  return val
         | 
| 1110 | 
            +
                end
         | 
| 1111 | 
            +
             | 
| 1112 | 
            +
                id.downcase.include?("spandrel")
         | 
| 1113 | 
            +
              end
         | 
| 1114 | 
            +
             | 
| 1115 | 
            +
              # ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
         | 
| 1116 | 
            +
              # ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
         | 
| 1117 | 
            +
              # This next set of utilities (~850 lines) help distinguish spaces that are
         | 
| 1118 | 
            +
              # directly vs indirectly CONDITIONED, vs SEMIHEATED. The solution here
         | 
| 49 1119 | 
             
              # relies as much as possible on space conditioning categories found in
         | 
| 50 | 
            -
              # standards like ASHRAE 90.1 and energy codes like the Canadian  | 
| 1120 | 
            +
              # standards like ASHRAE 90.1 and energy codes like the Canadian NECBs.
         | 
| 1121 | 
            +
              #
         | 
| 51 1122 | 
             
              # Both documents share many similarities, regardless of nomenclature. There
         | 
| 52 1123 | 
             
              # are however noticeable differences between approaches on how a space is
         | 
| 53 1124 | 
             
              # tagged as falling into one of the aforementioned categories. First, an
         | 
| @@ -69,11 +1140,11 @@ module OSut | |
| 69 1140 | 
             
              #
         | 
| 70 1141 | 
             
              #               ... includes plenums, atria, etc.
         | 
| 71 1142 | 
             
              #
         | 
| 72 | 
            -
              #     -  | 
| 1143 | 
            +
              #     - SEMIHEATED space: an ENCLOSED space that has a heating system
         | 
| 73 1144 | 
             
              #       >= 10 W/m2, yet NOT a CONDITIONED space (see above).
         | 
| 74 1145 | 
             
              #
         | 
| 75 1146 | 
             
              #     - UNCONDITIONED space: an ENCLOSED space that is NOT a conditioned
         | 
| 76 | 
            -
              #       space or a  | 
| 1147 | 
            +
              #       space or a SEMIHEATED space (see above).
         | 
| 77 1148 | 
             
              #
         | 
| 78 1149 | 
             
              #       NOTE: Crawlspaces, attics, and parking garages with natural or
         | 
| 79 1150 | 
             
              #       mechanical ventilation are considered UNENCLOSED spaces.
         | 
| @@ -94,41 +1165,116 @@ module OSut | |
| 94 1165 | 
             
              # to ASHRAE 90.1 (e.g. heating and/or cooling based, no distinction for
         | 
| 95 1166 | 
             
              # INDIRECTLY conditioned spaces like plenums).
         | 
| 96 1167 | 
             
              #
         | 
| 97 | 
            -
              #  | 
| 98 | 
            -
              # is based on desired/intended design space setpoint | 
| 99 | 
            -
              # system sizing criteria. No further treatment | 
| 100 | 
            -
              # distinguish  | 
| 1168 | 
            +
              # SEMIHEATED spaces are described in the NECB (yet not a defined term). The
         | 
| 1169 | 
            +
              # distinction is also based on desired/intended design space setpoint
         | 
| 1170 | 
            +
              # temperatures (here 15°C) - not system sizing criteria. No further treatment
         | 
| 1171 | 
            +
              # is implemented here to distinguish SEMIHEATED from CONDITIONED spaces;
         | 
| 1172 | 
            +
              # notwithstanding the AdditionalProperties tag (described further in this
         | 
| 1173 | 
            +
              # section), it is up to users to determine if a CONDITIONED space is
         | 
| 1174 | 
            +
              # indeed SEMIHEATED or not (e.g. based on MIN/MAX setpoints).
         | 
| 101 1175 | 
             
              #
         | 
| 102 1176 | 
             
              # The single NECB criterion distinguishing UNCONDITIONED ENCLOSED spaces
         | 
| 103 1177 | 
             
              # (such as vestibules) from UNENCLOSED spaces (such as attics) remains the
         | 
| 104 1178 | 
             
              # intention to ventilate - or rather to what degree. Regardless, the methods
         | 
| 105 | 
            -
              # here are designed to process both classifications in the same way, namely | 
| 106 | 
            -
              # focusing on adjacent surfaces to CONDITIONED (or  | 
| 107 | 
            -
              # of the building envelope.
         | 
| 108 | 
            -
             | 
| 109 | 
            -
              # In light of the above, methods here are designed without a priori | 
| 110 | 
            -
              # of explicit system sizing choices or access to iterative | 
| 111 | 
            -
              # processes. As discussed in greater detail  | 
| 112 | 
            -
              # rely on zoning  | 
| 1179 | 
            +
              # here are designed to process both classifications in the same way, namely
         | 
| 1180 | 
            +
              # by focusing on adjacent surfaces to CONDITIONED (or SEMIHEATED) spaces as
         | 
| 1181 | 
            +
              # part of the building envelope.
         | 
| 1182 | 
            +
             | 
| 1183 | 
            +
              # In light of the above, OSut methods here are designed without a priori
         | 
| 1184 | 
            +
              # knowledge of explicit system sizing choices or access to iterative
         | 
| 1185 | 
            +
              # autosizing processes. As discussed in greater detail below, methods here
         | 
| 1186 | 
            +
              # are developed to rely on zoning and/or "intended" temperature setpoints.
         | 
| 1187 | 
            +
              # In addition, OSut methods here cannot distinguish between UNCONDITIONED vs
         | 
| 1188 | 
            +
              # UNENCLOSED spaces from OpenStudio geometry alone. They are henceforth
         | 
| 1189 | 
            +
              # considered synonymous.
         | 
| 113 1190 | 
             
              #
         | 
| 114 1191 | 
             
              # For an OpenStudio model in an incomplete or preliminary state, e.g. holding
         | 
| 115 | 
            -
              # fully-formed ENCLOSED spaces  | 
| 116 | 
            -
              # temperatures (early design stage assessments of form, porosity or | 
| 117 | 
            -
              #  | 
| 118 | 
            -
              #  | 
| 1192 | 
            +
              # fully-formed ENCLOSED spaces WITHOUT thermal zoning information or setpoint
         | 
| 1193 | 
            +
              # temperatures (early design stage assessments of form, porosity or
         | 
| 1194 | 
            +
              # envelope), OpenStudio spaces are considered CONDITIONED by default. This
         | 
| 1195 | 
            +
              # default behaviour may be reset based on the (Space) AdditionalProperties
         | 
| 1196 | 
            +
              # "space_conditioning_category" key (4x possible values), which is relied
         | 
| 1197 | 
            +
              # upon by OpenStudio-Standards:
         | 
| 1198 | 
            +
              #
         | 
| 1199 | 
            +
              #   github.com/NREL/openstudio-standards/blob/
         | 
| 1200 | 
            +
              #   d2b5e28928e712cb3f137ab5c1ad6d8889ca02b7/lib/openstudio-standards/
         | 
| 1201 | 
            +
              #   standards/Standards.Space.rb#L1604C5-L1605C1
         | 
| 1202 | 
            +
              #
         | 
| 1203 | 
            +
              # OpenStudio-Standards recognizes 4x possible value strings:
         | 
| 1204 | 
            +
              #   - "NonResConditioned"
         | 
| 1205 | 
            +
              #   - "ResConditioned"
         | 
| 1206 | 
            +
              #   - "Semiheated"
         | 
| 1207 | 
            +
              #   - "Unconditioned"
         | 
| 1208 | 
            +
              #
         | 
| 1209 | 
            +
              # OSut maintains existing "space_conditioning_category" key/value pairs
         | 
| 1210 | 
            +
              # intact. Based on these, OSut methods may return related outputs:
         | 
| 1211 | 
            +
              #
         | 
| 1212 | 
            +
              #   "space_conditioning_category" | OSut status   | heating °C | cooling °C
         | 
| 1213 | 
            +
              # -------------------------------   -------------   ----------   ----------
         | 
| 1214 | 
            +
              #   - "NonResConditioned"           CONDITIONED     21.0         24.0
         | 
| 1215 | 
            +
              #   - "ResConditioned"              CONDITIONED     21.0         24.0
         | 
| 1216 | 
            +
              #   - "Semiheated"                  SEMIHEATED      15.0         NA
         | 
| 1217 | 
            +
              #   - "Unconditioned"               UNCONDITIONED   NA           NA
         | 
| 1218 | 
            +
              #
         | 
| 1219 | 
            +
              # OSut also looks up another (Space) AdditionalProperties 'key',
         | 
| 1220 | 
            +
              # "indirectlyconditioned" to flag plenum or occupied spaces indirectly
         | 
| 1221 | 
            +
              # conditioned with transfer air only. The only accepted 'value' for an
         | 
| 1222 | 
            +
              # "indirectlyconditioned" 'key' is the name (string) of another (linked)
         | 
| 1223 | 
            +
              # space, e.g.:
         | 
| 1224 | 
            +
              #
         | 
| 1225 | 
            +
              #   "indirectlyconditioned" space | linked space, e.g. "core_space"
         | 
| 1226 | 
            +
              # -------------------------------   ---------------------------------------
         | 
| 1227 | 
            +
              #   return air plenum               occupied space below
         | 
| 1228 | 
            +
              #   supply air plenum               occupied space above
         | 
| 1229 | 
            +
              #   dead air space (not a plenum)   nearby occupied space
         | 
| 1230 | 
            +
              #
         | 
| 1231 | 
            +
              # OSut doesn't validate whether the "indirectlyconditioned" space is actually
         | 
| 1232 | 
            +
              # adjacent to its linked space. It nonetheless relies on the latter's
         | 
| 1233 | 
            +
              # conditioning category (e.g. CONDITIONED, SEMIHEATED) to determine
         | 
| 1234 | 
            +
              # anticipated ambient temperatures in the former. For instance, an
         | 
| 1235 | 
            +
              # "indirectlyconditioned"-tagged return air plenum linked to a SEMIHEATED
         | 
| 1236 | 
            +
              # space is considered as free-floating in terms of cooling, and unlikely to
         | 
| 1237 | 
            +
              # have ambient conditions below 15°C under heating (winter) design
         | 
| 1238 | 
            +
              # conditions. OSut will associate this plenum to a 15°C heating setpoint
         | 
| 1239 | 
            +
              # temperature. If the SEMIHEATED space instead has a heating setpoint
         | 
| 1240 | 
            +
              # temperature of 7°C, then OSut will associate a 7°C heating setpoint to this
         | 
| 1241 | 
            +
              # plenum.
         | 
| 1242 | 
            +
              #
         | 
| 1243 | 
            +
              # Even when a (more developed) OpenStudio model holds valid space/zone
         | 
| 1244 | 
            +
              # temperature setpoints, OSut gives priority to these AdditionalProperties.
         | 
| 1245 | 
            +
              # For instance, a CONDITIONED space can be considered INDIRECTLYCONDITIONED,
         | 
| 1246 | 
            +
              # even if its zone thermostat has a valid heating and/or cooling setpoint.
         | 
| 1247 | 
            +
              # This is in sync with OpenStudio-Standards' method
         | 
| 1248 | 
            +
              # "space_conditioning_category()".
         | 
| 1249 | 
            +
             | 
| 1250 | 
            +
              ##
         | 
| 1251 | 
            +
              # Validates if model has zones with HVAC air loops.
         | 
| 1252 | 
            +
              #
         | 
| 1253 | 
            +
              # @param model [OpenStudio::Model::Model] a model
         | 
| 119 1254 | 
             
              #
         | 
| 120 | 
            -
              #  | 
| 121 | 
            -
              #  | 
| 122 | 
            -
               | 
| 123 | 
            -
             | 
| 1255 | 
            +
              # @return [Bool] whether model has HVAC air loops
         | 
| 1256 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 1257 | 
            +
              def airLoopsHVAC?(model = nil)
         | 
| 1258 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 1259 | 
            +
                cl  = OpenStudio::Model::Model
         | 
| 1260 | 
            +
                return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
         | 
| 1261 | 
            +
             | 
| 1262 | 
            +
                model.getThermalZones.each do |zone|
         | 
| 1263 | 
            +
                  next            if zone.canBePlenum
         | 
| 1264 | 
            +
                  return true unless zone.airLoopHVACs.empty?
         | 
| 1265 | 
            +
                  return true     if zone.isPlenum
         | 
| 1266 | 
            +
                end
         | 
| 1267 | 
            +
             | 
| 1268 | 
            +
                false
         | 
| 1269 | 
            +
              end
         | 
| 124 1270 |  | 
| 125 1271 | 
             
              ##
         | 
| 126 | 
            -
              #  | 
| 1272 | 
            +
              # Returns MIN/MAX values of a schedule (ruleset).
         | 
| 127 1273 | 
             
              #
         | 
| 128 | 
            -
              # @param sched [OpenStudio::Model::ScheduleRuleset] schedule
         | 
| 1274 | 
            +
              # @param sched [OpenStudio::Model::ScheduleRuleset] a schedule
         | 
| 129 1275 | 
             
              #
         | 
| 130 1276 | 
             
              # @return [Hash] min: (Float), max: (Float)
         | 
| 131 | 
            -
              # @return [Hash] min: nil, max: nil  | 
| 1277 | 
            +
              # @return [Hash] min: nil, max: nil if invalid inputs (see logs)
         | 
| 132 1278 | 
             
              def scheduleRulesetMinMax(sched = nil)
         | 
| 133 1279 | 
             
                # Largely inspired from David Goldwasser's
         | 
| 134 1280 | 
             
                # "schedule_ruleset_annual_min_max_value":
         | 
| @@ -139,44 +1285,28 @@ module OSut | |
| 139 1285 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 140 1286 | 
             
                cl  = OpenStudio::Model::ScheduleRuleset
         | 
| 141 1287 | 
             
                res = { min: nil, max: nil }
         | 
| 142 | 
            -
             | 
| 143 | 
            -
                return invalid("sched", mth, 1, DBG, res)     unless sched.respond_to?(NS)
         | 
| 1288 | 
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         | 
| 144 1289 |  | 
| 145 1290 | 
             
                id = sched.nameString
         | 
| 146 1291 | 
             
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         | 
| 147 1292 |  | 
| 148 | 
            -
                 | 
| 149 | 
            -
                profiles << sched.defaultDaySchedule
         | 
| 150 | 
            -
                sched.scheduleRules.each { |rule| profiles << rule.daySchedule }
         | 
| 1293 | 
            +
                values = sched.defaultDaySchedule.values.to_a
         | 
| 151 1294 |  | 
| 152 | 
            -
                 | 
| 153 | 
            -
                  id = profile.nameString
         | 
| 154 | 
            -
             | 
| 155 | 
            -
                  profile.values.each do |val|
         | 
| 156 | 
            -
                    ok = val.is_a?(Numeric)
         | 
| 157 | 
            -
                    log(WRN, "Skipping non-numeric value in '#{id}' (#{mth})")     unless ok
         | 
| 158 | 
            -
                    next                                                           unless ok
         | 
| 159 | 
            -
             | 
| 160 | 
            -
                    res[:min] = val unless res[:min]
         | 
| 161 | 
            -
                    res[:min] = val     if res[:min] > val
         | 
| 162 | 
            -
                    res[:max] = val unless res[:max]
         | 
| 163 | 
            -
                    res[:max] = val     if res[:max] < val
         | 
| 164 | 
            -
                  end
         | 
| 165 | 
            -
                end
         | 
| 1295 | 
            +
                sched.scheduleRules.each { |rule| values += rule.daySchedule.values }
         | 
| 166 1296 |  | 
| 167 | 
            -
                 | 
| 168 | 
            -
                 | 
| 1297 | 
            +
                res[:min] = values.min.is_a?(Numeric) ? values.min : nil
         | 
| 1298 | 
            +
                res[:max] = values.max.is_a?(Numeric) ? values.max : nil
         | 
| 169 1299 |  | 
| 170 1300 | 
             
                res
         | 
| 171 1301 | 
             
              end
         | 
| 172 1302 |  | 
| 173 1303 | 
             
              ##
         | 
| 174 | 
            -
              #  | 
| 1304 | 
            +
              # Returns MIN/MAX values of a schedule (constant).
         | 
| 175 1305 | 
             
              #
         | 
| 176 | 
            -
              # @param sched [OpenStudio::Model::ScheduleConstant] schedule
         | 
| 1306 | 
            +
              # @param sched [OpenStudio::Model::ScheduleConstant] a schedule
         | 
| 177 1307 | 
             
              #
         | 
| 178 1308 | 
             
              # @return [Hash] min: (Float), max: (Float)
         | 
| 179 | 
            -
              # @return [Hash] min: nil, max: nil  | 
| 1309 | 
            +
              # @return [Hash] min: nil, max: nil if invalid inputs (see logs)
         | 
| 180 1310 | 
             
              def scheduleConstantMinMax(sched = nil)
         | 
| 181 1311 | 
             
                # Largely inspired from David Goldwasser's
         | 
| 182 1312 | 
             
                # "schedule_constant_annual_min_max_value":
         | 
| @@ -187,14 +1317,13 @@ module OSut | |
| 187 1317 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 188 1318 | 
             
                cl  = OpenStudio::Model::ScheduleConstant
         | 
| 189 1319 | 
             
                res = { min: nil, max: nil }
         | 
| 190 | 
            -
             | 
| 191 | 
            -
                return invalid("sched", mth, 1, DBG, res)     unless sched.respond_to?(NS)
         | 
| 1320 | 
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         | 
| 192 1321 |  | 
| 193 1322 | 
             
                id = sched.nameString
         | 
| 194 1323 | 
             
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         | 
| 195 1324 |  | 
| 196 | 
            -
                 | 
| 197 | 
            -
                mismatch(" | 
| 1325 | 
            +
                ok = sched.value.is_a?(Numeric)
         | 
| 1326 | 
            +
                mismatch("#{id} value", sched.value, Numeric, mth, ERR, res) unless ok
         | 
| 198 1327 | 
             
                res[:min] = sched.value
         | 
| 199 1328 | 
             
                res[:max] = sched.value
         | 
| 200 1329 |  | 
| @@ -202,12 +1331,12 @@ module OSut | |
| 202 1331 | 
             
              end
         | 
| 203 1332 |  | 
| 204 1333 | 
             
              ##
         | 
| 205 | 
            -
              #  | 
| 1334 | 
            +
              # Returns MIN/MAX values of a schedule (compact).
         | 
| 206 1335 | 
             
              #
         | 
| 207 1336 | 
             
              # @param sched [OpenStudio::Model::ScheduleCompact] schedule
         | 
| 208 1337 | 
             
              #
         | 
| 209 1338 | 
             
              # @return [Hash] min: (Float), max: (Float)
         | 
| 210 | 
            -
              # @return [Hash] min: nil, max: nil  | 
| 1339 | 
            +
              # @return [Hash] min: nil, max: nil if invalid input (see logs)
         | 
| 211 1340 | 
             
              def scheduleCompactMinMax(sched = nil)
         | 
| 212 1341 | 
             
                # Largely inspired from Andrew Parker's
         | 
| 213 1342 | 
             
                # "schedule_compact_annual_min_max_value":
         | 
| @@ -215,78 +1344,69 @@ module OSut | |
| 215 1344 | 
             
                # github.com/NREL/openstudio-standards/blob/
         | 
| 216 1345 | 
             
                # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
         | 
| 217 1346 | 
             
                # standards/Standards.ScheduleCompact.rb#L8
         | 
| 218 | 
            -
                mth | 
| 219 | 
            -
                cl | 
| 220 | 
            -
                vals | 
| 221 | 
            -
                 | 
| 222 | 
            -
                res | 
| 223 | 
            -
             | 
| 224 | 
            -
                return invalid("sched", mth, 1, DBG, res)     unless sched.respond_to?(NS)
         | 
| 1347 | 
            +
                mth  = "OSut::#{__callee__}"
         | 
| 1348 | 
            +
                cl   = OpenStudio::Model::ScheduleCompact
         | 
| 1349 | 
            +
                vals = []
         | 
| 1350 | 
            +
                prev = ""
         | 
| 1351 | 
            +
                res  = { min: nil, max: nil }
         | 
| 1352 | 
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         | 
| 225 1353 |  | 
| 226 1354 | 
             
                id = sched.nameString
         | 
| 227 1355 | 
             
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         | 
| 228 1356 |  | 
| 229 1357 | 
             
                sched.extensibleGroups.each do |eg|
         | 
| 230 | 
            -
                  if  | 
| 1358 | 
            +
                  if prev.include?("until")
         | 
| 231 1359 | 
             
                    vals << eg.getDouble(0).get unless eg.getDouble(0).empty?
         | 
| 232 1360 | 
             
                  end
         | 
| 233 1361 |  | 
| 234 | 
            -
                  str | 
| 235 | 
            -
                   | 
| 1362 | 
            +
                  str  = eg.getString(0)
         | 
| 1363 | 
            +
                  prev = str.get.downcase unless str.empty?
         | 
| 236 1364 | 
             
                end
         | 
| 237 1365 |  | 
| 238 | 
            -
                return empty(" | 
| 239 | 
            -
             | 
| 240 | 
            -
                ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
         | 
| 241 | 
            -
                log(ERR, "Non-numeric values in '#{id}' (#{mth})")                 unless ok
         | 
| 242 | 
            -
                return res                                                         unless ok
         | 
| 1366 | 
            +
                return empty("#{id} values", mth, ERR, res) if vals.empty?
         | 
| 243 1367 |  | 
| 244 | 
            -
                res[:min] = vals.min
         | 
| 245 | 
            -
                res[:max] = vals.max
         | 
| 1368 | 
            +
                res[:min] = vals.min.is_a?(Numeric) ? vals.min : nil
         | 
| 1369 | 
            +
                res[:max] = vals.min.is_a?(Numeric) ? vals.max : nil
         | 
| 246 1370 |  | 
| 247 1371 | 
             
                res
         | 
| 248 1372 | 
             
              end
         | 
| 249 1373 |  | 
| 250 1374 | 
             
              ##
         | 
| 251 | 
            -
              #  | 
| 1375 | 
            +
              # Returns MIN/MAX values for schedule (interval).
         | 
| 252 1376 | 
             
              #
         | 
| 253 1377 | 
             
              # @param sched [OpenStudio::Model::ScheduleInterval] schedule
         | 
| 254 1378 | 
             
              #
         | 
| 255 1379 | 
             
              # @return [Hash] min: (Float), max: (Float)
         | 
| 256 | 
            -
              # @return [Hash] min: nil, max: nil  | 
| 1380 | 
            +
              # @return [Hash] min: nil, max: nil if invalid input (see logs)
         | 
| 257 1381 | 
             
              def scheduleIntervalMinMax(sched = nil)
         | 
| 258 1382 | 
             
                mth  = "OSut::#{__callee__}"
         | 
| 259 1383 | 
             
                cl   = OpenStudio::Model::ScheduleInterval
         | 
| 260 1384 | 
             
                vals = []
         | 
| 261 1385 | 
             
                res  = { min: nil, max: nil }
         | 
| 262 | 
            -
             | 
| 263 | 
            -
                return invalid("sched", mth, 1, DBG, res)     unless sched.respond_to?(NS)
         | 
| 1386 | 
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         | 
| 264 1387 |  | 
| 265 1388 | 
             
                id = sched.nameString
         | 
| 266 1389 | 
             
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         | 
| 267 1390 |  | 
| 268 1391 | 
             
                vals = sched.timeSeries.values
         | 
| 269 | 
            -
                ok   = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
         | 
| 270 | 
            -
                log(ERR, "Non-numeric values in '#{id}' (#{mth})")                 unless ok
         | 
| 271 | 
            -
                return res                                                         unless ok
         | 
| 272 1392 |  | 
| 273 | 
            -
                res[:min] = vals.min
         | 
| 274 | 
            -
                res[:max] = vals.max
         | 
| 1393 | 
            +
                res[:min] = vals.min.is_a?(Numeric) ? vals.min : nil
         | 
| 1394 | 
            +
                res[:max] = vals.max.is_a?(Numeric) ? vals.min : nil
         | 
| 275 1395 |  | 
| 276 1396 | 
             
                res
         | 
| 277 1397 | 
             
              end
         | 
| 278 1398 |  | 
| 279 1399 | 
             
              ##
         | 
| 280 | 
            -
              #  | 
| 281 | 
            -
              # zone has active dual setpoint thermostat.
         | 
| 1400 | 
            +
              # Returns MAX zone heating temperature schedule setpoint [°C] and whether
         | 
| 1401 | 
            +
              # zone has an active dual setpoint thermostat.
         | 
| 282 1402 | 
             
              #
         | 
| 283 1403 | 
             
              # @param zone [OpenStudio::Model::ThermalZone] a thermal zone
         | 
| 284 1404 | 
             
              #
         | 
| 285 1405 | 
             
              # @return [Hash] spt: (Float), dual: (Bool)
         | 
| 286 | 
            -
              # @return [Hash] spt: nil, dual: false  | 
| 1406 | 
            +
              # @return [Hash] spt: nil, dual: false if invalid input (see logs)
         | 
| 287 1407 | 
             
              def maxHeatScheduledSetpoint(zone = nil)
         | 
| 288 1408 | 
             
                # Largely inspired from Parker & Marrec's "thermal_zone_heated?" procedure.
         | 
| 289 | 
            -
                # The solution here is a tad more relaxed to encompass  | 
| 1409 | 
            +
                # The solution here is a tad more relaxed to encompass SEMIHEATED zones as
         | 
| 290 1410 | 
             
                # per Canadian NECB criteria (basically any space with at least 10 W/m2 of
         | 
| 291 1411 | 
             
                # installed heating equipement, i.e. below freezing in Canada).
         | 
| 292 1412 | 
             
                #
         | 
| @@ -296,8 +1416,7 @@ module OSut | |
| 296 1416 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 297 1417 | 
             
                cl  = OpenStudio::Model::ThermalZone
         | 
| 298 1418 | 
             
                res = { spt: nil, dual: false }
         | 
| 299 | 
            -
             | 
| 300 | 
            -
                return invalid("zone", mth, 1, DBG, res)     unless zone.respond_to?(NS)
         | 
| 1419 | 
            +
                return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
         | 
| 301 1420 |  | 
| 302 1421 | 
             
                id = zone.nameString
         | 
| 303 1422 | 
             
                return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
         | 
| @@ -380,8 +1499,8 @@ module OSut | |
| 380 1499 |  | 
| 381 1500 | 
             
                return res if zone.thermostat.empty?
         | 
| 382 1501 |  | 
| 383 | 
            -
                tstat | 
| 384 | 
            -
                res[:spt] | 
| 1502 | 
            +
                tstat = zone.thermostat.get
         | 
| 1503 | 
            +
                res[:spt] = nil
         | 
| 385 1504 |  | 
| 386 1505 | 
             
                unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
         | 
| 387 1506 | 
             
                       tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
         | 
| @@ -394,7 +1513,7 @@ module OSut | |
| 394 1513 |  | 
| 395 1514 | 
             
                  unless tstat.heatingSetpointTemperatureSchedule.empty?
         | 
| 396 1515 | 
             
                    res[:dual] = true
         | 
| 397 | 
            -
                    sched | 
| 1516 | 
            +
                    sched = tstat.heatingSetpointTemperatureSchedule.get
         | 
| 398 1517 |  | 
| 399 1518 | 
             
                    unless sched.to_ScheduleRuleset.empty?
         | 
| 400 1519 | 
             
                      sched = sched.to_ScheduleRuleset.get
         | 
| @@ -453,16 +1572,15 @@ module OSut | |
| 453 1572 | 
             
              end
         | 
| 454 1573 |  | 
| 455 1574 | 
             
              ##
         | 
| 456 | 
            -
              #  | 
| 1575 | 
            +
              # Validates if model has zones with valid heating temperature setpoints.
         | 
| 457 1576 | 
             
              #
         | 
| 458 1577 | 
             
              # @param model [OpenStudio::Model::Model] a model
         | 
| 459 1578 | 
             
              #
         | 
| 460 | 
            -
              # @return [Bool]  | 
| 461 | 
            -
              # @return [ | 
| 1579 | 
            +
              # @return [Bool] whether model holds valid heating temperature setpoints
         | 
| 1580 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 462 1581 | 
             
              def heatingTemperatureSetpoints?(model = nil)
         | 
| 463 1582 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 464 1583 | 
             
                cl  = OpenStudio::Model::Model
         | 
| 465 | 
            -
             | 
| 466 1584 | 
             
                return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
         | 
| 467 1585 |  | 
| 468 1586 | 
             
                model.getThermalZones.each do |zone|
         | 
| @@ -473,13 +1591,13 @@ module OSut | |
| 473 1591 | 
             
              end
         | 
| 474 1592 |  | 
| 475 1593 | 
             
              ##
         | 
| 476 | 
            -
              #  | 
| 477 | 
            -
              # zone has active dual setpoint thermostat.
         | 
| 1594 | 
            +
              # Returns MIN zone cooling temperature schedule setpoint [°C] and whether
         | 
| 1595 | 
            +
              # zone has an active dual setpoint thermostat.
         | 
| 478 1596 | 
             
              #
         | 
| 479 1597 | 
             
              # @param zone [OpenStudio::Model::ThermalZone] a thermal zone
         | 
| 480 1598 | 
             
              #
         | 
| 481 1599 | 
             
              # @return [Hash] spt: (Float), dual: (Bool)
         | 
| 482 | 
            -
              # @return [Hash] spt: nil, dual: false  | 
| 1600 | 
            +
              # @return [Hash] spt: nil, dual: false if invalid input (see logs)
         | 
| 483 1601 | 
             
              def minCoolScheduledSetpoint(zone = nil)
         | 
| 484 1602 | 
             
                # Largely inspired from Parker & Marrec's "thermal_zone_cooled?" procedure.
         | 
| 485 1603 | 
             
                #
         | 
| @@ -489,8 +1607,7 @@ module OSut | |
| 489 1607 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 490 1608 | 
             
                cl  = OpenStudio::Model::ThermalZone
         | 
| 491 1609 | 
             
                res = { spt: nil, dual: false }
         | 
| 492 | 
            -
             | 
| 493 | 
            -
                return invalid("zone", mth, 1, DBG, res)     unless zone.respond_to?(NS)
         | 
| 1610 | 
            +
                return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
         | 
| 494 1611 |  | 
| 495 1612 | 
             
                id = zone.nameString
         | 
| 496 1613 | 
             
                return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
         | 
| @@ -560,8 +1677,8 @@ module OSut | |
| 560 1677 |  | 
| 561 1678 | 
             
                return res if zone.thermostat.empty?
         | 
| 562 1679 |  | 
| 563 | 
            -
                tstat | 
| 564 | 
            -
                res[:spt] | 
| 1680 | 
            +
                tstat     = zone.thermostat.get
         | 
| 1681 | 
            +
                res[:spt] = nil
         | 
| 565 1682 |  | 
| 566 1683 | 
             
                unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
         | 
| 567 1684 | 
             
                       tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
         | 
| @@ -574,7 +1691,7 @@ module OSut | |
| 574 1691 |  | 
| 575 1692 | 
             
                  unless tstat.coolingSetpointTemperatureSchedule.empty?
         | 
| 576 1693 | 
             
                    res[:dual] = true
         | 
| 577 | 
            -
                    sched | 
| 1694 | 
            +
                    sched = tstat.coolingSetpointTemperatureSchedule.get
         | 
| 578 1695 |  | 
| 579 1696 | 
             
                    unless sched.to_ScheduleRuleset.empty?
         | 
| 580 1697 | 
             
                      sched = sched.to_ScheduleRuleset.get
         | 
| @@ -633,17 +1750,16 @@ module OSut | |
| 633 1750 | 
             
              end
         | 
| 634 1751 |  | 
| 635 1752 | 
             
              ##
         | 
| 636 | 
            -
              #  | 
| 1753 | 
            +
              # Validates if model has zones with valid cooling temperature setpoints.
         | 
| 637 1754 | 
             
              #
         | 
| 638 1755 | 
             
              # @param model [OpenStudio::Model::Model] a model
         | 
| 639 1756 | 
             
              #
         | 
| 640 | 
            -
              # @return [Bool]  | 
| 641 | 
            -
              # @return [ | 
| 1757 | 
            +
              # @return [Bool] whether model holds valid cooling temperature setpoints
         | 
| 1758 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 642 1759 | 
             
              def coolingTemperatureSetpoints?(model = nil)
         | 
| 643 1760 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 644 1761 | 
             
                cl  = OpenStudio::Model::Model
         | 
| 645 | 
            -
             | 
| 646 | 
            -
                return mismatch("model", model, cl, mth, DBG, false)  unless model.is_a?(cl)
         | 
| 1762 | 
            +
                return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
         | 
| 647 1763 |  | 
| 648 1764 | 
             
                model.getThermalZones.each do |zone|
         | 
| 649 1765 | 
             
                  return true if minCoolScheduledSetpoint(zone)[:spt]
         | 
| @@ -653,117 +1769,318 @@ module OSut | |
| 653 1769 | 
             
              end
         | 
| 654 1770 |  | 
| 655 1771 | 
             
              ##
         | 
| 656 | 
            -
              #  | 
| 1772 | 
            +
              # Validates whether space is a vestibule.
         | 
| 657 1773 | 
             
              #
         | 
| 658 | 
            -
              # @param  | 
| 1774 | 
            +
              # @param space [OpenStudio::Model::Space] a space
         | 
| 659 1775 | 
             
              #
         | 
| 660 | 
            -
              # @return [Bool]  | 
| 661 | 
            -
              # @return [ | 
| 662 | 
            -
              def  | 
| 1776 | 
            +
              # @return [Bool] whether space is considered a vestibule
         | 
| 1777 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 1778 | 
            +
              def vestibule?(space = nil)
         | 
| 1779 | 
            +
                # INFO: OpenStudio-Standards' "thermal_zone_vestibule" criteria:
         | 
| 1780 | 
            +
                #   - zones less than 200ft2; AND
         | 
| 1781 | 
            +
                #   - having infiltration using Design Flow Rate
         | 
| 1782 | 
            +
                #
         | 
| 1783 | 
            +
                #   github.com/NREL/openstudio-standards/blob/
         | 
| 1784 | 
            +
                #   86bcd026a20001d903cc613bed6d63e94b14b142/lib/openstudio-standards/
         | 
| 1785 | 
            +
                #   standards/Standards.ThermalZone.rb#L1264
         | 
| 1786 | 
            +
                #
         | 
| 1787 | 
            +
                # This (unused) OpenStudio-Standards method likely needs revision; it would
         | 
| 1788 | 
            +
                # return "false" if the thermal zone area were less than 200ft2. Not sure
         | 
| 1789 | 
            +
                # which edition of 90.1 relies on a 200ft2 threshold (2010?); 90.1 2016
         | 
| 1790 | 
            +
                # doesn't. Yet even fixed, the method would nonetheless misidentify as
         | 
| 1791 | 
            +
                # "vestibule" a small space along an exterior wall, such as a semiheated
         | 
| 1792 | 
            +
                # storage space.
         | 
| 1793 | 
            +
                #
         | 
| 1794 | 
            +
                # The code below is intended as a simple short-term solution, basically
         | 
| 1795 | 
            +
                # relying on AdditionalProperties, or (if missing) a "vestibule" substring
         | 
| 1796 | 
            +
                # within a space's spaceType name (or the latter's standardsSpaceType).
         | 
| 1797 | 
            +
                #
         | 
| 1798 | 
            +
                # Alternatively, some future method could infer its status as a vestibule
         | 
| 1799 | 
            +
                # based on a few basic features (common to all vintages):
         | 
| 1800 | 
            +
                #   - 1x+ outdoor-facing wall(s) holding 1x+ door(s)
         | 
| 1801 | 
            +
                #   - adjacent to 1x+ 'occupied' conditioned space(s)
         | 
| 1802 | 
            +
                #   - ideally, 1x+ door(s) between vestibule and 1x+ such adjacent space(s)
         | 
| 1803 | 
            +
                #
         | 
| 1804 | 
            +
                # An additional method parameter (i.e. std = :necb) could be added to
         | 
| 1805 | 
            +
                # ensure supplementary Standard-specific checks, e.g. maximum floor area,
         | 
| 1806 | 
            +
                # minimum distance between doors.
         | 
| 1807 | 
            +
                #
         | 
| 1808 | 
            +
                # Finally, an entirely separate method could be developed to first identify
         | 
| 1809 | 
            +
                # whether "building entrances" (a defined term in 90.1) actually require
         | 
| 1810 | 
            +
                # vestibules as per specific code requirements. Food for thought.
         | 
| 663 1811 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 664 | 
            -
                cl  = OpenStudio::Model:: | 
| 1812 | 
            +
                cl  = OpenStudio::Model::Space
         | 
| 1813 | 
            +
                return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
         | 
| 665 1814 |  | 
| 666 | 
            -
                 | 
| 1815 | 
            +
                id  = space.nameString
         | 
| 1816 | 
            +
                m1  = "#{id}:vestibule"
         | 
| 1817 | 
            +
                m2  = "#{id}:vestibule:boolean"
         | 
| 667 1818 |  | 
| 668 | 
            -
                 | 
| 669 | 
            -
                   | 
| 670 | 
            -
                  return  | 
| 671 | 
            -
             | 
| 1819 | 
            +
                if space.additionalProperties.hasFeature("vestibule")
         | 
| 1820 | 
            +
                  val = space.additionalProperties.getFeatureAsBoolean("vestibule")
         | 
| 1821 | 
            +
                  return invalid(m1, mth, 1, ERR, false) if val.empty?
         | 
| 1822 | 
            +
             | 
| 1823 | 
            +
                  val = val.get
         | 
| 1824 | 
            +
                  return invalid(m2, mth, 1, ERR, false) unless [true, false].include?(val)
         | 
| 1825 | 
            +
                  return val
         | 
| 1826 | 
            +
                end
         | 
| 1827 | 
            +
             | 
| 1828 | 
            +
                unless space.spaceType.empty?
         | 
| 1829 | 
            +
                  type = space.spaceType.get
         | 
| 1830 | 
            +
                  return false if type.nameString.downcase.include?("plenum")
         | 
| 1831 | 
            +
                  return true  if type.nameString.downcase.include?("vestibule")
         | 
| 1832 | 
            +
             | 
| 1833 | 
            +
                  unless type.standardsSpaceType.empty?
         | 
| 1834 | 
            +
                    type = type.standardsSpaceType.get.downcase
         | 
| 1835 | 
            +
                    return false if type.include?("plenum")
         | 
| 1836 | 
            +
                    return true  if type.include?("vestibule")
         | 
| 1837 | 
            +
                  end
         | 
| 672 1838 | 
             
                end
         | 
| 673 1839 |  | 
| 674 1840 | 
             
                false
         | 
| 675 1841 | 
             
              end
         | 
| 676 1842 |  | 
| 677 1843 | 
             
              ##
         | 
| 678 | 
            -
              #  | 
| 1844 | 
            +
              # Validates whether a space is an indirectly-conditioned plenum.
         | 
| 679 1845 | 
             
              #
         | 
| 680 1846 | 
             
              # @param space [OpenStudio::Model::Space] a space
         | 
| 681 | 
            -
              # @param loops [Bool] true if model has airLoopHVAC object(s)
         | 
| 682 | 
            -
              # @param setpoints [Bool] true if model has valid temperature setpoints
         | 
| 683 1847 | 
             
              #
         | 
| 684 | 
            -
              # @return [Bool]  | 
| 685 | 
            -
              # @return [ | 
| 686 | 
            -
              def plenum?(space = nil | 
| 687 | 
            -
                # Largely inspired from NREL's "space_plenum?" | 
| 1848 | 
            +
              # @return [Bool] whether space is considered a plenum
         | 
| 1849 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 1850 | 
            +
              def plenum?(space = nil)
         | 
| 1851 | 
            +
                # Largely inspired from NREL's "space_plenum?":
         | 
| 688 1852 | 
             
                #
         | 
| 689 | 
            -
                # | 
| 690 | 
            -
                # | 
| 691 | 
            -
                # | 
| 1853 | 
            +
                #   github.com/NREL/openstudio-standards/blob/
         | 
| 1854 | 
            +
                #   58964222d25783e9da4ae292e375fb0d5c902aa5/lib/openstudio-standards/
         | 
| 1855 | 
            +
                #   standards/Standards.Space.rb#L1384
         | 
| 1856 | 
            +
                #
         | 
| 1857 | 
            +
                # Ideally, "plenum?" should be in sync with OpenStudio SDK's "isPlenum"
         | 
| 1858 | 
            +
                # method, which solely looks for either HVAC air mixer objects:
         | 
| 1859 | 
            +
                #  - AirLoopHVACReturnPlenum
         | 
| 1860 | 
            +
                #  - AirLoopHVACSupplyPlenum
         | 
| 1861 | 
            +
                #
         | 
| 1862 | 
            +
                # Of the OpenStudio-Standards Prototype models, only the LargeOffice
         | 
| 1863 | 
            +
                # holds AirLoopHVACReturnPlenum objects. OpenStudio-Standards' method
         | 
| 1864 | 
            +
                # "space_plenum?" indeed catches them by checking if the space is
         | 
| 1865 | 
            +
                # "partofTotalFloorArea" (which internally has an "isPlenum" check). So
         | 
| 1866 | 
            +
                # "isPlenum" closely follows ASHRAE 90.1 2016's definition of "plenum":
         | 
| 1867 | 
            +
                #
         | 
| 1868 | 
            +
                #   "plenum": a compartment or chamber ...
         | 
| 1869 | 
            +
                #             - to which one or more ducts are connected
         | 
| 1870 | 
            +
                #             - that forms a part of the air distribution system, and
         | 
| 1871 | 
            +
                #             - that is NOT USED for occupancy or storage.
         | 
| 1872 | 
            +
                #
         | 
| 1873 | 
            +
                # Canadian NECB 2020 has the following (not as well) defined term:
         | 
| 1874 | 
            +
                #   "plenum": a chamber forming part of an air duct system.
         | 
| 1875 | 
            +
                #             ... we'll assume that a space shall also be considered
         | 
| 1876 | 
            +
                #             UNOCCUPIED if it's "part of an air duct system".
         | 
| 1877 | 
            +
                #
         | 
| 1878 | 
            +
                # As intended, "isPlenum" would NOT identify as a "plenum" any vented
         | 
| 1879 | 
            +
                # UNCONDITIONED or UNENCLOSED attic or crawlspace - good. Yet "isPlenum"
         | 
| 1880 | 
            +
                # would also ignore dead air spaces integrating ducted return air. The
         | 
| 1881 | 
            +
                # SDK's "partofTotalFloorArea" would be more suitable in such cases, as
         | 
| 1882 | 
            +
                # long as modellers have, a priori, set this parameter to FALSE.
         | 
| 1883 | 
            +
                #
         | 
| 1884 | 
            +
                # OpenStudio-Standards' "space_plenum?" catches a MUCH WIDER range of
         | 
| 1885 | 
            +
                # spaces, which aren't caught by "isPlenum". This includes attics,
         | 
| 1886 | 
            +
                # crawlspaces, non-plenum air spaces above ceiling tiles, and any other
         | 
| 1887 | 
            +
                # UNOCCUPIED space in a model. The term "plenum" in this context is more
         | 
| 1888 | 
            +
                # of a catch-all shorthand - to be used with caution. For instance,
         | 
| 1889 | 
            +
                # "space_plenum?" shouldn't be used (in isolation) to determine whether an
         | 
| 1890 | 
            +
                # UNOCCUPIED space should have its envelope insulated ("plenum") or not
         | 
| 1891 | 
            +
                # ("attic").
         | 
| 1892 | 
            +
                #
         | 
| 1893 | 
            +
                # In contrast to OpenStudio-Standards' "space_plenum?", this method
         | 
| 1894 | 
            +
                # strictly returns FALSE if a space is indeed "partofTotalFloorArea". It
         | 
| 1895 | 
            +
                # also returns FALSE if the space is a vestibule. Otherwise, it needs more
         | 
| 1896 | 
            +
                # information to determine if such an UNOCCUPIED space is indeed a
         | 
| 1897 | 
            +
                # plenum. Beyond these 2x criteria, a space is considered a plenum if:
         | 
| 692 1898 | 
             
                #
         | 
| 693 | 
            -
                # A  | 
| 1899 | 
            +
                # CASE A: it includes the substring "plenum" (case insensitive) in its
         | 
| 1900 | 
            +
                #         spaceType's name, or in the latter's standardsSpaceType string;
         | 
| 694 1901 | 
             
                #
         | 
| 695 | 
            -
                # CASE  | 
| 696 | 
            -
                #         OpenStudio model (complete with HVAC air loops); OR
         | 
| 1902 | 
            +
                # CASE B: "isPlenum" == TRUE in an OpenStudio model WITH HVAC airloops: OR
         | 
| 697 1903 | 
             
                #
         | 
| 698 | 
            -
                # CASE  | 
| 699 | 
            -
                #          | 
| 700 | 
            -
                #         thermostat, i.e. can't extract valid setpoints; OR
         | 
| 1904 | 
            +
                # CASE C: its zone holds an 'inactive' thermostat (i.e. can't extract valid
         | 
| 1905 | 
            +
                #         setpoints) in an OpenStudio model with setpoint temperatures.
         | 
| 701 1906 | 
             
                #
         | 
| 702 | 
            -
                #  | 
| 703 | 
            -
                # | 
| 704 | 
            -
                # | 
| 1907 | 
            +
                # If a modeller is instead simply interested in identifying UNOCCUPIED
         | 
| 1908 | 
            +
                # spaces that are INDIRECTLYCONDITIONED (not necessarily plenums), then the
         | 
| 1909 | 
            +
                # following combination is likely more reliable and less confusing:
         | 
| 1910 | 
            +
                #   - SDK's partofTotalFloorArea == FALSE
         | 
| 1911 | 
            +
                #   - OSut's unconditioned? == FALSE
         | 
| 705 1912 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 706 1913 | 
             
                cl  = OpenStudio::Model::Space
         | 
| 707 | 
            -
             | 
| 708 | 
            -
                return  | 
| 1914 | 
            +
                return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
         | 
| 1915 | 
            +
                return false if space.partofTotalFloorArea
         | 
| 1916 | 
            +
                return false if vestibule?(space)
         | 
| 709 1917 |  | 
| 710 1918 | 
             
                id = space.nameString
         | 
| 711 | 
            -
                 | 
| 1919 | 
            +
                m1 = "#{id}:plenum"
         | 
| 1920 | 
            +
                m1 = "#{id}:plenum boolean"
         | 
| 1921 | 
            +
             | 
| 1922 | 
            +
                # CASE A: "plenum" spaceType.
         | 
| 1923 | 
            +
                unless space.spaceType.empty?
         | 
| 1924 | 
            +
                  type = space.spaceType.get
         | 
| 1925 | 
            +
                  return true if type.nameString.downcase.include?("plenum")
         | 
| 1926 | 
            +
             | 
| 1927 | 
            +
                  unless type.standardsSpaceType.empty?
         | 
| 1928 | 
            +
                    type = type.standardsSpaceType.get.downcase
         | 
| 1929 | 
            +
                    return true if type.include?("plenum")
         | 
| 1930 | 
            +
                  end
         | 
| 1931 | 
            +
                end
         | 
| 1932 | 
            +
             | 
| 1933 | 
            +
                # CASE B: "isPlenum" == TRUE if airloops.
         | 
| 1934 | 
            +
                return space.isPlenum if airLoopsHVAC?(space.model)
         | 
| 1935 | 
            +
             | 
| 1936 | 
            +
                # CASE C: zone holds an 'inactive' thermostat.
         | 
| 1937 | 
            +
                zone   = space.thermalZone
         | 
| 1938 | 
            +
                heated = heatingTemperatureSetpoints?(space.model)
         | 
| 1939 | 
            +
                cooled = coolingTemperatureSetpoints?(space.model)
         | 
| 1940 | 
            +
             | 
| 1941 | 
            +
                if heated || cooled
         | 
| 1942 | 
            +
                  return false if zone.empty?
         | 
| 1943 | 
            +
             | 
| 1944 | 
            +
                  zone = zone.get
         | 
| 1945 | 
            +
                  heat = maxHeatScheduledSetpoint(zone)
         | 
| 1946 | 
            +
                  cool = minCoolScheduledSetpoint(zone)
         | 
| 1947 | 
            +
                  return false if heat[:spt] || cool[:spt] # directly CONDITIONED
         | 
| 1948 | 
            +
                  return heat[:dual] || cool[:dual]        # FALSE if both are nilled
         | 
| 1949 | 
            +
                end
         | 
| 1950 | 
            +
             | 
| 1951 | 
            +
                false
         | 
| 1952 | 
            +
              end
         | 
| 1953 | 
            +
             | 
| 1954 | 
            +
              ##
         | 
| 1955 | 
            +
              # Retrieves a space's (implicit or explicit) heating/cooling setpoints.
         | 
| 1956 | 
            +
              #
         | 
| 1957 | 
            +
              # @param space [OpenStudio::Model::Space] a space
         | 
| 1958 | 
            +
              #
         | 
| 1959 | 
            +
              # @return [Hash] heating: (Float), cooling: (Float)
         | 
| 1960 | 
            +
              # @return [Hash] heating: nil, cooling: nil if invalid input (see logs)
         | 
| 1961 | 
            +
              def setpoints(space = nil)
         | 
| 1962 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 1963 | 
            +
                cl1 = OpenStudio::Model::Space
         | 
| 1964 | 
            +
                cl2 = String
         | 
| 1965 | 
            +
                res = {heating: nil, cooling: nil}
         | 
| 1966 | 
            +
                tg1 = "space_conditioning_category"
         | 
| 1967 | 
            +
                tg2 = "indirectlyconditioned"
         | 
| 1968 | 
            +
                cts = ["nonresconditioned", "resconditioned", "semiheated", "unconditioned"]
         | 
| 1969 | 
            +
                cnd = nil
         | 
| 1970 | 
            +
                return mismatch("space", space, cl1, mth, DBG, res) unless space.is_a?(cl1)
         | 
| 1971 | 
            +
             | 
| 1972 | 
            +
                # 1. Check for OpenStudio-Standards' space conditioning categories.
         | 
| 1973 | 
            +
                if space.additionalProperties.hasFeature(tg1)
         | 
| 1974 | 
            +
                  cnd = space.additionalProperties.getFeatureAsString(tg1)
         | 
| 1975 | 
            +
             | 
| 1976 | 
            +
                  if cnd.empty?
         | 
| 1977 | 
            +
                    cnd = nil
         | 
| 1978 | 
            +
                  else
         | 
| 1979 | 
            +
                    cnd = cnd.get
         | 
| 712 1980 |  | 
| 713 | 
            -
             | 
| 714 | 
            -
             | 
| 1981 | 
            +
                    if cts.include?(cnd.downcase)
         | 
| 1982 | 
            +
                      return res if cnd.downcase == "unconditioned"
         | 
| 1983 | 
            +
                    else
         | 
| 1984 | 
            +
                      invalid("#{tg1}:#{cnd}", mth, 0, ERR)
         | 
| 1985 | 
            +
                      cnd = nil
         | 
| 1986 | 
            +
                    end
         | 
| 1987 | 
            +
                  end
         | 
| 1988 | 
            +
                end
         | 
| 715 1989 |  | 
| 716 | 
            -
                 | 
| 717 | 
            -
                 | 
| 1990 | 
            +
                # 2. Check instead OSut's INDIRECTLYCONDITIONED (parent space) link.
         | 
| 1991 | 
            +
                if cnd.nil?
         | 
| 1992 | 
            +
                  id = space.additionalProperties.getFeatureAsString(tg2)
         | 
| 718 1993 |  | 
| 719 | 
            -
             | 
| 720 | 
            -
             | 
| 721 | 
            -
             | 
| 1994 | 
            +
                  unless id.empty?
         | 
| 1995 | 
            +
                    id  = id.get
         | 
| 1996 | 
            +
                    dad = space.model.getSpaceByName(id)
         | 
| 722 1997 |  | 
| 723 | 
            -
             | 
| 724 | 
            -
             | 
| 725 | 
            -
                     | 
| 726 | 
            -
             | 
| 727 | 
            -
             | 
| 728 | 
            -
             | 
| 1998 | 
            +
                    if dad.empty?
         | 
| 1999 | 
            +
                      log(ERR, "Unknown space #{id} (#{mth})")
         | 
| 2000 | 
            +
                    else
         | 
| 2001 | 
            +
                      # Now focus on 'parent' space linked to INDIRECTLYCONDITIONED space.
         | 
| 2002 | 
            +
                      space = dad.get
         | 
| 2003 | 
            +
                      cnd   = tg2
         | 
| 2004 | 
            +
                    end
         | 
| 729 2005 | 
             
                  end
         | 
| 730 2006 | 
             
                end
         | 
| 731 2007 |  | 
| 732 | 
            -
                 | 
| 733 | 
            -
             | 
| 734 | 
            -
             | 
| 2008 | 
            +
                # 3. Fetch space setpoints (if model indeed holds valid setpoints).
         | 
| 2009 | 
            +
                heated = heatingTemperatureSetpoints?(space.model)
         | 
| 2010 | 
            +
                cooled = coolingTemperatureSetpoints?(space.model)
         | 
| 2011 | 
            +
                zone   = space.thermalZone
         | 
| 2012 | 
            +
             | 
| 2013 | 
            +
                if heated || cooled
         | 
| 2014 | 
            +
                  return res if zone.empty? # UNCONDITIONED
         | 
| 2015 | 
            +
             | 
| 2016 | 
            +
                  zone = zone.get
         | 
| 2017 | 
            +
                  res[:heating] = maxHeatScheduledSetpoint(zone)[:spt]
         | 
| 2018 | 
            +
                  res[:cooling] = minCoolScheduledSetpoint(zone)[:spt]
         | 
| 735 2019 | 
             
                end
         | 
| 736 2020 |  | 
| 737 | 
            -
                 | 
| 738 | 
            -
             | 
| 739 | 
            -
                   | 
| 2021 | 
            +
                # 4. Reset if AdditionalProperties were found & valid.
         | 
| 2022 | 
            +
                unless cnd.nil?
         | 
| 2023 | 
            +
                  if cnd.downcase == "unconditioned"
         | 
| 2024 | 
            +
                    res[:heating] = nil
         | 
| 2025 | 
            +
                    res[:cooling] = nil
         | 
| 2026 | 
            +
                  elsif cnd.downcase == "semiheated"
         | 
| 2027 | 
            +
                    res[:heating] = 15.0 if res[:heating].nil?
         | 
| 2028 | 
            +
                    res[:cooling] = nil
         | 
| 2029 | 
            +
                  elsif cnd.downcase.include?("conditioned")
         | 
| 2030 | 
            +
                    # "nonresconditioned", "resconditioned" or "indirectlyconditioned"
         | 
| 2031 | 
            +
                    res[:heating] = 21.0 if res[:heating].nil? # default
         | 
| 2032 | 
            +
                    res[:cooling] = 24.0 if res[:cooling].nil? # default
         | 
| 2033 | 
            +
                  end
         | 
| 740 2034 | 
             
                end
         | 
| 741 2035 |  | 
| 742 | 
            -
                 | 
| 2036 | 
            +
                # 5. Reset if plenum?
         | 
| 2037 | 
            +
                if plenum?(space)
         | 
| 2038 | 
            +
                  res[:heating] = 21.0 if res[:heating].nil? # default
         | 
| 2039 | 
            +
                  res[:cooling] = 24.0 if res[:cooling].nil? # default
         | 
| 2040 | 
            +
                end
         | 
| 2041 | 
            +
             | 
| 2042 | 
            +
                res
         | 
| 2043 | 
            +
              end
         | 
| 2044 | 
            +
             | 
| 2045 | 
            +
              ##
         | 
| 2046 | 
            +
              # Validates if a space is UNCONDITIONED.
         | 
| 2047 | 
            +
              #
         | 
| 2048 | 
            +
              # @param space [OpenStudio::Model::Space] a space
         | 
| 2049 | 
            +
              #
         | 
| 2050 | 
            +
              # @return [Bool] whether space is considered UNCONDITIONED
         | 
| 2051 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2052 | 
            +
              def unconditioned?(space = nil)
         | 
| 2053 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2054 | 
            +
                cl  = OpenStudio::Model::Space
         | 
| 2055 | 
            +
                return mismatch("space", space, cl, mth, DBG, false) unless space.is_a?(cl)
         | 
| 2056 | 
            +
             | 
| 2057 | 
            +
                ok = false
         | 
| 2058 | 
            +
                ok = setpoints(space)[:heating].nil? && setpoints(space)[:cooling].nil?
         | 
| 2059 | 
            +
             | 
| 2060 | 
            +
                ok
         | 
| 743 2061 | 
             
              end
         | 
| 744 2062 |  | 
| 745 2063 | 
             
              ##
         | 
| 746 | 
            -
              #  | 
| 2064 | 
            +
              # Generates an HVAC availability schedule.
         | 
| 747 2065 | 
             
              #
         | 
| 748 2066 | 
             
              # @param model [OpenStudio::Model::Model] a model
         | 
| 749 2067 | 
             
              # @param avl [String] seasonal availability choice (optional, default "ON")
         | 
| 750 2068 | 
             
              #
         | 
| 751 2069 | 
             
              # @return [OpenStudio::Model::Schedule] HVAC availability sched
         | 
| 752 | 
            -
              # @return [ | 
| 2070 | 
            +
              # @return [nil] if invalid input (see logs)
         | 
| 753 2071 | 
             
              def availabilitySchedule(model = nil, avl = "")
         | 
| 754 2072 | 
             
                mth    = "OSut::#{__callee__}"
         | 
| 755 2073 | 
             
                cl     = OpenStudio::Model::Model
         | 
| 756 2074 | 
             
                limits = nil
         | 
| 757 | 
            -
             | 
| 758 | 
            -
                return  | 
| 759 | 
            -
                return invalid("availability", avl, 2, mth)    unless avl.respond_to?(:to_s)
         | 
| 2075 | 
            +
                return mismatch("model",     model, cl, mth) unless model.is_a?(cl)
         | 
| 2076 | 
            +
                return invalid("availability", avl,  2, mth) unless avl.respond_to?(:to_s)
         | 
| 760 2077 |  | 
| 761 2078 | 
             
                # Either fetch availability ScheduleTypeLimits object, or create one.
         | 
| 762 2079 | 
             
                model.getScheduleTypeLimitss.each do |l|
         | 
| 763 | 
            -
                  break | 
| 764 | 
            -
                  next | 
| 765 | 
            -
                  next | 
| 766 | 
            -
                  next | 
| 2080 | 
            +
                  break    if limits
         | 
| 2081 | 
            +
                  next     if l.lowerLimitValue.empty?
         | 
| 2082 | 
            +
                  next     if l.upperLimitValue.empty?
         | 
| 2083 | 
            +
                  next     if l.numericType.empty?
         | 
| 767 2084 | 
             
                  next unless l.lowerLimitValue.get.to_i == 0
         | 
| 768 2085 | 
             
                  next unless l.upperLimitValue.get.to_i == 1
         | 
| 769 2086 | 
             
                  next unless l.numericType.get.downcase == "discrete"
         | 
| @@ -789,35 +2106,35 @@ module OSut | |
| 789 2106 |  | 
| 790 2107 | 
             
                # Seasonal availability start/end dates.
         | 
| 791 2108 | 
             
                year  = model.yearDescription
         | 
| 792 | 
            -
                return | 
| 2109 | 
            +
                return empty("yearDescription", mth, ERR) if year.empty?
         | 
| 793 2110 |  | 
| 794 2111 | 
             
                year  = year.get
         | 
| 795 2112 | 
             
                may01 = year.makeDate(OpenStudio::MonthOfYear.new("May"),  1)
         | 
| 796 2113 | 
             
                oct31 = year.makeDate(OpenStudio::MonthOfYear.new("Oct"), 31)
         | 
| 797 2114 |  | 
| 798 | 
            -
                case avl. | 
| 799 | 
            -
                when "winter" | 
| 2115 | 
            +
                case trim(avl).downcase
         | 
| 2116 | 
            +
                when "winter" # available from November 1 to April 30 (6 months)
         | 
| 800 2117 | 
             
                  val = 1
         | 
| 801 2118 | 
             
                  sch = off
         | 
| 802 2119 | 
             
                  nom = "WINTER Availability SchedRuleset"
         | 
| 803 2120 | 
             
                  dft = "WINTER Availability dftDaySched"
         | 
| 804 2121 | 
             
                  tag = "May-Oct WINTER Availability SchedRule"
         | 
| 805 2122 | 
             
                  day = "May-Oct WINTER SchedRule Day"
         | 
| 806 | 
            -
                when "summer" | 
| 2123 | 
            +
                when "summer" # available from May 1 to October 31 (6 months)
         | 
| 807 2124 | 
             
                  val = 0
         | 
| 808 2125 | 
             
                  sch = on
         | 
| 809 2126 | 
             
                  nom = "SUMMER Availability SchedRuleset"
         | 
| 810 2127 | 
             
                  dft = "SUMMER Availability dftDaySched"
         | 
| 811 2128 | 
             
                  tag = "May-Oct SUMMER Availability SchedRule"
         | 
| 812 2129 | 
             
                  day = "May-Oct SUMMER SchedRule Day"
         | 
| 813 | 
            -
                when "off" | 
| 2130 | 
            +
                when "off" # never available
         | 
| 814 2131 | 
             
                  val = 0
         | 
| 815 2132 | 
             
                  sch = on
         | 
| 816 2133 | 
             
                  nom = "OFF Availability SchedRuleset"
         | 
| 817 2134 | 
             
                  dft = "OFF Availability dftDaySched"
         | 
| 818 2135 | 
             
                  tag = ""
         | 
| 819 2136 | 
             
                  day = ""
         | 
| 820 | 
            -
                else | 
| 2137 | 
            +
                else # always available
         | 
| 821 2138 | 
             
                  val = 1
         | 
| 822 2139 | 
             
                  sch = on
         | 
| 823 2140 | 
             
                  nom = "ON Availability SchedRuleset"
         | 
| @@ -835,14 +2152,14 @@ module OSut | |
| 835 2152 |  | 
| 836 2153 | 
             
                  unless schedule.empty?
         | 
| 837 2154 | 
             
                    schedule = schedule.get
         | 
| 838 | 
            -
                    default | 
| 2155 | 
            +
                    default  = schedule.defaultDaySchedule
         | 
| 839 2156 | 
             
                    ok = ok && default.nameString           == dft
         | 
| 840 2157 | 
             
                    ok = ok && default.times.size           == 1
         | 
| 841 2158 | 
             
                    ok = ok && default.values.size          == 1
         | 
| 842 2159 | 
             
                    ok = ok && default.times.first          == time
         | 
| 843 2160 | 
             
                    ok = ok && default.values.first         == val
         | 
| 844 2161 | 
             
                    rules = schedule.scheduleRules
         | 
| 845 | 
            -
                    ok = ok &&  | 
| 2162 | 
            +
                    ok = ok && rules.size < 2
         | 
| 846 2163 |  | 
| 847 2164 | 
             
                    if rules.size == 1
         | 
| 848 2165 | 
             
                      rule = rules.first
         | 
| @@ -867,30 +2184,37 @@ module OSut | |
| 867 2184 |  | 
| 868 2185 | 
             
                schedule = OpenStudio::Model::ScheduleRuleset.new(model)
         | 
| 869 2186 | 
             
                schedule.setName(nom)
         | 
| 870 | 
            -
                ok = schedule.setScheduleTypeLimits(limits)
         | 
| 871 | 
            -
                log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})")      unless ok
         | 
| 872 | 
            -
                return nil                                                         unless ok
         | 
| 873 2187 |  | 
| 874 | 
            -
                 | 
| 875 | 
            -
             | 
| 876 | 
            -
             | 
| 2188 | 
            +
                unless schedule.setScheduleTypeLimits(limits)
         | 
| 2189 | 
            +
                  log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})")
         | 
| 2190 | 
            +
                  return nil
         | 
| 2191 | 
            +
                end
         | 
| 2192 | 
            +
             | 
| 2193 | 
            +
                unless schedule.defaultDaySchedule.addValue(time, val)
         | 
| 2194 | 
            +
                  log(ERR, "'#{nom}': Can't set default day schedule (#{mth})")
         | 
| 2195 | 
            +
                  return nil
         | 
| 2196 | 
            +
                end
         | 
| 877 2197 |  | 
| 878 2198 | 
             
                schedule.defaultDaySchedule.setName(dft)
         | 
| 879 2199 |  | 
| 880 2200 | 
             
                unless tag.empty?
         | 
| 881 2201 | 
             
                  rule = OpenStudio::Model::ScheduleRule.new(schedule, sch)
         | 
| 882 2202 | 
             
                  rule.setName(tag)
         | 
| 883 | 
            -
                  ok = rule.setStartDate(may01)
         | 
| 884 | 
            -
                  log(ERR, "'#{tag}': Can't set start date (#{mth})")              unless ok
         | 
| 885 | 
            -
                  return nil                                                       unless ok
         | 
| 886 2203 |  | 
| 887 | 
            -
                   | 
| 888 | 
            -
             | 
| 889 | 
            -
             | 
| 2204 | 
            +
                  unless rule.setStartDate(may01)
         | 
| 2205 | 
            +
                    log(ERR, "'#{tag}': Can't set start date (#{mth})")
         | 
| 2206 | 
            +
                    return nil
         | 
| 2207 | 
            +
                  end
         | 
| 2208 | 
            +
             | 
| 2209 | 
            +
                  unless rule.setEndDate(oct31)
         | 
| 2210 | 
            +
                    log(ERR, "'#{tag}': Can't set end date (#{mth})")
         | 
| 2211 | 
            +
                    return nil
         | 
| 2212 | 
            +
                  end
         | 
| 890 2213 |  | 
| 891 | 
            -
                   | 
| 892 | 
            -
             | 
| 893 | 
            -
             | 
| 2214 | 
            +
                  unless rule.setApplyAllDays(true)
         | 
| 2215 | 
            +
                    log(ERR, "'#{tag}': Can't apply to all days (#{mth})")
         | 
| 2216 | 
            +
                    return nil
         | 
| 2217 | 
            +
                  end
         | 
| 894 2218 |  | 
| 895 2219 | 
             
                  rule.daySchedule.setName(day)
         | 
| 896 2220 | 
             
                end
         | 
| @@ -898,615 +2222,756 @@ module OSut | |
| 898 2222 | 
             
                schedule
         | 
| 899 2223 | 
             
              end
         | 
| 900 2224 |  | 
| 2225 | 
            +
              # ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
         | 
| 2226 | 
            +
              # ---- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---- #
         | 
| 2227 | 
            +
              # This final set of utilities targets OpenStudio geometry. Many of the
         | 
| 2228 | 
            +
              # following geometry methods rely on Boost as an OpenStudio dependency.
         | 
| 2229 | 
            +
              # As per Boost requirements, points (e.g. polygons) must first be 'aligned':
         | 
| 2230 | 
            +
              #   - first rotated/tilted as to lay flat along XY plane (Z-axis ~= 0)
         | 
| 2231 | 
            +
              #   - initial Z-axis values are represented as Y-axis values
         | 
| 2232 | 
            +
              #   - points with the lowest X-axis values are 'aligned' along X-axis (0)
         | 
| 2233 | 
            +
              #   - points with the lowest Z-axis values are 'aligned' along Y-axis (0)
         | 
| 2234 | 
            +
              #   - for several Boost methods, points must be clockwise in sequence
         | 
| 2235 | 
            +
              #
         | 
| 2236 | 
            +
              # Check OSut's poly() method, which offers such Boost-related options.
         | 
| 2237 | 
            +
             | 
| 901 2238 | 
             
              ##
         | 
| 902 | 
            -
              #  | 
| 2239 | 
            +
              # Returns OpenStudio site/space transformation & rotation angle [0,2PI) rads.
         | 
| 903 2240 | 
             
              #
         | 
| 904 | 
            -
              # @param  | 
| 905 | 
            -
              # @param bse [OpensStudio::Model::ConstructionBase] a construction base
         | 
| 906 | 
            -
              # @param gr [Bool] true if ground-facing surface
         | 
| 907 | 
            -
              # @param ex [Bool] true if exterior-facing surface
         | 
| 908 | 
            -
              # @param typ [String] a surface type
         | 
| 2241 | 
            +
              # @param group [OpenStudio::Model::PlanarSurfaceGroup] a site or space object
         | 
| 909 2242 | 
             
              #
         | 
| 910 | 
            -
              # @return [ | 
| 911 | 
            -
              # @return [ | 
| 912 | 
            -
              def  | 
| 2243 | 
            +
              # @return [Hash] t: (OpenStudio::Transformation), r: (Float)
         | 
| 2244 | 
            +
              # @return [Hash] t: nil, r: nil if invalid input (see logs)
         | 
| 2245 | 
            +
              def transforms(group = nil)
         | 
| 913 2246 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 914 | 
            -
                 | 
| 915 | 
            -
                 | 
| 916 | 
            -
             | 
| 917 | 
            -
                return invalid("set", mth, 1, DBG, false)         unless set.respond_to?(NS)
         | 
| 918 | 
            -
             | 
| 919 | 
            -
                id = set.nameString
         | 
| 920 | 
            -
                return mismatch(id, set, cl1, mth, DBG, false)    unless set.is_a?(cl1)
         | 
| 921 | 
            -
                return invalid("base", mth, 2, DBG, false)        unless bse.respond_to?(NS)
         | 
| 922 | 
            -
             | 
| 923 | 
            -
                id = bse.nameString
         | 
| 924 | 
            -
                return mismatch(id, bse, cl2, mth, DBG, false)    unless bse.is_a?(cl2)
         | 
| 925 | 
            -
             | 
| 926 | 
            -
                valid = gr == true || gr == false
         | 
| 927 | 
            -
                return invalid("ground", mth, 3, DBG, false)      unless valid
         | 
| 928 | 
            -
             | 
| 929 | 
            -
                valid = ex == true || ex == false
         | 
| 930 | 
            -
                return invalid("exterior", mth, 4, DBG, false)    unless valid
         | 
| 931 | 
            -
             | 
| 932 | 
            -
                valid = typ.respond_to?(:to_s)
         | 
| 933 | 
            -
                return invalid("surface typ", mth, 4, DBG, false) unless valid
         | 
| 934 | 
            -
             | 
| 935 | 
            -
                type = typ.to_s.downcase
         | 
| 936 | 
            -
                valid = type == "floor" || type == "wall" || type == "roofceiling"
         | 
| 937 | 
            -
                return invalid("surface type", mth, 5, DBG, false) unless valid
         | 
| 2247 | 
            +
                cl2 = OpenStudio::Model::PlanarSurfaceGroup
         | 
| 2248 | 
            +
                res = { t: nil, r: nil }
         | 
| 2249 | 
            +
                return invalid("group", mth, 2, DBG, res) unless group.respond_to?(NS)
         | 
| 938 2250 |  | 
| 939 | 
            -
                 | 
| 2251 | 
            +
                id  = group.nameString
         | 
| 2252 | 
            +
                mdl = group.model
         | 
| 2253 | 
            +
                return mismatch(id, group, cl2, mth, DBG, res) unless group.is_a?(cl2)
         | 
| 940 2254 |  | 
| 941 | 
            -
                 | 
| 942 | 
            -
             | 
| 943 | 
            -
                    constructions = set.defaultGroundContactSurfaceConstructions.get
         | 
| 944 | 
            -
                  end
         | 
| 945 | 
            -
                elsif ex
         | 
| 946 | 
            -
                  unless set.defaultExteriorSurfaceConstructions.empty?
         | 
| 947 | 
            -
                    constructions = set.defaultExteriorSurfaceConstructions.get
         | 
| 948 | 
            -
                  end
         | 
| 949 | 
            -
                else
         | 
| 950 | 
            -
                  unless set.defaultInteriorSurfaceConstructions.empty?
         | 
| 951 | 
            -
                    constructions = set.defaultInteriorSurfaceConstructions.get
         | 
| 952 | 
            -
                  end
         | 
| 953 | 
            -
                end
         | 
| 2255 | 
            +
                res[:t] = group.siteTransformation
         | 
| 2256 | 
            +
                res[:r] = group.directionofRelativeNorth + mdl.getBuilding.northAxis
         | 
| 954 2257 |  | 
| 955 | 
            -
                 | 
| 2258 | 
            +
                res
         | 
| 2259 | 
            +
              end
         | 
| 956 2260 |  | 
| 957 | 
            -
             | 
| 958 | 
            -
             | 
| 959 | 
            -
             | 
| 960 | 
            -
             | 
| 961 | 
            -
             | 
| 962 | 
            -
             | 
| 963 | 
            -
             | 
| 964 | 
            -
             | 
| 965 | 
            -
             | 
| 966 | 
            -
             | 
| 967 | 
            -
             | 
| 968 | 
            -
                 | 
| 969 | 
            -
             | 
| 970 | 
            -
                    construction = constructions.wallConstruction.get
         | 
| 971 | 
            -
                    return true if construction == bse
         | 
| 972 | 
            -
                  end
         | 
| 973 | 
            -
                end
         | 
| 2261 | 
            +
              ##
         | 
| 2262 | 
            +
              # Returns true if 2 OpenStudio 3D points are nearly equal
         | 
| 2263 | 
            +
              #
         | 
| 2264 | 
            +
              # @param p1 [OpenStudio::Point3d] 1st 3D point
         | 
| 2265 | 
            +
              # @param p2 [OpenStudio::Point3d] 2nd 3D point
         | 
| 2266 | 
            +
              #
         | 
| 2267 | 
            +
              # @return [Bool] whether equal points (within TOL)
         | 
| 2268 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2269 | 
            +
              def same?(p1 = nil, p2 = nil)
         | 
| 2270 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2271 | 
            +
                cl  = OpenStudio::Point3d
         | 
| 2272 | 
            +
                return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
         | 
| 2273 | 
            +
                return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
         | 
| 974 2274 |  | 
| 975 | 
            -
                 | 
| 2275 | 
            +
                # OpenStudio.isAlmostEqual3dPt(p1, p2, TOL) # ... from v350 onwards.
         | 
| 2276 | 
            +
                (p1.x-p2.x).abs < TOL && (p1.y-p2.y).abs < TOL && (p1.z-p2.z).abs < TOL
         | 
| 976 2277 | 
             
              end
         | 
| 977 2278 |  | 
| 978 2279 | 
             
              ##
         | 
| 979 | 
            -
              #  | 
| 2280 | 
            +
              # Returns true if a line segment is along the X-axis.
         | 
| 980 2281 | 
             
              #
         | 
| 981 | 
            -
              # @param  | 
| 982 | 
            -
              # @param  | 
| 2282 | 
            +
              # @param p1 [OpenStudio::Point3d] 1st 3D point of a line segment
         | 
| 2283 | 
            +
              # @param p2 [OpenStudio::Point3d] 2nd 3D point of a line segment
         | 
| 2284 | 
            +
              # @param strict [Bool] whether segment shouldn't hold Y- or Z-axis components
         | 
| 983 2285 | 
             
              #
         | 
| 984 | 
            -
              # @return [ | 
| 985 | 
            -
              # @return [ | 
| 986 | 
            -
              def  | 
| 2286 | 
            +
              # @return [Bool] whether along the X-axis
         | 
| 2287 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2288 | 
            +
              def xx?(p1 = nil, p2 = nil, strict = true)
         | 
| 987 2289 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 988 | 
            -
                 | 
| 989 | 
            -
                 | 
| 990 | 
            -
             | 
| 991 | 
            -
                return mismatch(" | 
| 992 | 
            -
                return  | 
| 2290 | 
            +
                cl  = OpenStudio::Point3d
         | 
| 2291 | 
            +
                strict = true unless [true, false].include?(strict)
         | 
| 2292 | 
            +
                return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
         | 
| 2293 | 
            +
                return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
         | 
| 2294 | 
            +
                return false if (p1.y - p2.y).abs > TOL && strict
         | 
| 2295 | 
            +
                return false if (p1.z - p2.z).abs > TOL && strict
         | 
| 2296 | 
            +
             | 
| 2297 | 
            +
                (p1.x - p2.x).abs > TOL
         | 
| 2298 | 
            +
              end
         | 
| 993 2299 |  | 
| 994 | 
            -
             | 
| 995 | 
            -
             | 
| 2300 | 
            +
              ##
         | 
| 2301 | 
            +
              # Returns true if a line segment is along the Y-axis.
         | 
| 2302 | 
            +
              #
         | 
| 2303 | 
            +
              # @param p1 [OpenStudio::Point3d] 1st 3D point of a line segment
         | 
| 2304 | 
            +
              # @param p2 [OpenStudio::Point3d] 2nd 3D point of a line segment
         | 
| 2305 | 
            +
              # @param strict [Bool] whether segment shouldn't hold X- or Z-axis components
         | 
| 2306 | 
            +
              #
         | 
| 2307 | 
            +
              # @return [Bool] whether along the Y-axis
         | 
| 2308 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2309 | 
            +
              def yy?(p1 = nil, p2 = nil, strict = true)
         | 
| 2310 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2311 | 
            +
                cl  = OpenStudio::Point3d
         | 
| 2312 | 
            +
                strict = true unless [true, false].include?(strict)
         | 
| 2313 | 
            +
                return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
         | 
| 2314 | 
            +
                return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
         | 
| 2315 | 
            +
                return false if (p1.x - p2.x).abs > TOL && strict
         | 
| 2316 | 
            +
                return false if (p1.z - p2.z).abs > TOL && strict
         | 
| 2317 | 
            +
             | 
| 2318 | 
            +
                (p1.y - p2.y).abs > TOL
         | 
| 2319 | 
            +
              end
         | 
| 996 2320 |  | 
| 997 | 
            -
             | 
| 998 | 
            -
             | 
| 999 | 
            -
             | 
| 1000 | 
            -
             | 
| 2321 | 
            +
              ##
         | 
| 2322 | 
            +
              # Returns true if a line segment is along the Z-axis.
         | 
| 2323 | 
            +
              #
         | 
| 2324 | 
            +
              # @param p1 [OpenStudio::Point3d] 1st 3D point of a line segment
         | 
| 2325 | 
            +
              # @param p2 [OpenStudio::Point3d] 2nd 3D point of a line segment
         | 
| 2326 | 
            +
              # @param strict [Bool] whether segment shouldn't hold X- or Y-axis components
         | 
| 2327 | 
            +
              #
         | 
| 2328 | 
            +
              # @return [Bool] whether along the Z-axis
         | 
| 2329 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2330 | 
            +
              def zz?(p1 = nil, p2 = nil, strict = true)
         | 
| 2331 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2332 | 
            +
                cl  = OpenStudio::Point3d
         | 
| 2333 | 
            +
                strict = true unless [true, false].include?(strict)
         | 
| 2334 | 
            +
                return mismatch("point 1", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
         | 
| 2335 | 
            +
                return mismatch("point 2", p2, cl, mth, DBG, false) unless p2.is_a?(cl)
         | 
| 2336 | 
            +
                return false if (p1.x - p2.x).abs > TOL && strict
         | 
| 2337 | 
            +
                return false if (p1.y - p2.y).abs > TOL && strict
         | 
| 2338 | 
            +
             | 
| 2339 | 
            +
                (p1.z - p2.z).abs > TOL
         | 
| 2340 | 
            +
              end
         | 
| 1001 2341 |  | 
| 1002 | 
            -
             | 
| 1003 | 
            -
             | 
| 2342 | 
            +
              ##
         | 
| 2343 | 
            +
              # Returns a scalar product of an OpenStudio Vector3d.
         | 
| 2344 | 
            +
              #
         | 
| 2345 | 
            +
              # @param v [OpenStudio::Vector3d] a vector
         | 
| 2346 | 
            +
              # @param m [#to_f] a scalar
         | 
| 2347 | 
            +
              #
         | 
| 2348 | 
            +
              # @return [OpenStudio::Vector3d] scaled points (see logs if empty)
         | 
| 2349 | 
            +
              def scalar(v = OpenStudio::Vector3d.new, m = 0)
         | 
| 2350 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2351 | 
            +
                cl  = OpenStudio::Vector3d
         | 
| 2352 | 
            +
                ok  = m.respond_to?(:to_f)
         | 
| 2353 | 
            +
                return mismatch("vector", v, cl,      mth, DBG, v) unless v.is_a?(cl)
         | 
| 2354 | 
            +
                return mismatch("m",      m, Numeric, mth, DBG, v) unless ok
         | 
| 1004 2355 |  | 
| 1005 | 
            -
                 | 
| 1006 | 
            -
                 | 
| 1007 | 
            -
             | 
| 1008 | 
            -
                exterior = false
         | 
| 2356 | 
            +
                m = m.to_f
         | 
| 2357 | 
            +
                OpenStudio::Vector3d.new(m * v.x, m * v.y, m * v.z)
         | 
| 2358 | 
            +
              end
         | 
| 1009 2359 |  | 
| 1010 | 
            -
             | 
| 1011 | 
            -
             | 
| 1012 | 
            -
             | 
| 1013 | 
            -
             | 
| 1014 | 
            -
             | 
| 2360 | 
            +
              ##
         | 
| 2361 | 
            +
              # Returns OpenStudio 3D points as an OpenStudio point vector, validating
         | 
| 2362 | 
            +
              # points in the process (if Array).
         | 
| 2363 | 
            +
              #
         | 
| 2364 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
         | 
| 2365 | 
            +
              #
         | 
| 2366 | 
            +
              # @return [OpenStudio::Point3dVector] 3D vector (see logs if empty)
         | 
| 2367 | 
            +
              def to_p3Dv(pts = nil)
         | 
| 2368 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2369 | 
            +
                cl1 = Array
         | 
| 2370 | 
            +
                cl2 = OpenStudio::Point3dVector
         | 
| 2371 | 
            +
                cl3 = OpenStudio::Model::PlanarSurface
         | 
| 2372 | 
            +
                cl4 = OpenStudio::Point3d
         | 
| 2373 | 
            +
                v   = OpenStudio::Point3dVector.new
         | 
| 2374 | 
            +
                return pts                                           if pts.is_a?(cl2)
         | 
| 2375 | 
            +
                return pts.vertices                                  if pts.is_a?(cl3)
         | 
| 2376 | 
            +
                return mismatch("points", pts, cl1, mth, DBG, v) unless pts.is_a?(cl1)
         | 
| 1015 2377 |  | 
| 1016 | 
            -
                 | 
| 1017 | 
            -
                   | 
| 1018 | 
            -
                  return set        if holdsConstruction?(set, base, ground, exterior, type)
         | 
| 2378 | 
            +
                pts.each do |pt|
         | 
| 2379 | 
            +
                  return mismatch("point", pt, cl4, mth, DBG, v) unless pt.is_a?(cl4)
         | 
| 1019 2380 | 
             
                end
         | 
| 1020 2381 |  | 
| 1021 | 
            -
                 | 
| 1022 | 
            -
                  spacetype = space.spaceType.get
         | 
| 2382 | 
            +
                pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, pt.z) }
         | 
| 1023 2383 |  | 
| 1024 | 
            -
             | 
| 1025 | 
            -
             | 
| 1026 | 
            -
                    return set      if holdsConstruction?(set, base, ground, exterior, type)
         | 
| 1027 | 
            -
                  end
         | 
| 1028 | 
            -
                end
         | 
| 2384 | 
            +
                v
         | 
| 2385 | 
            +
              end
         | 
| 1029 2386 |  | 
| 1030 | 
            -
             | 
| 1031 | 
            -
             | 
| 2387 | 
            +
              ##
         | 
| 2388 | 
            +
              # Returns true if an OpenStudio 3D point is part of a set of 3D points.
         | 
| 2389 | 
            +
              #
         | 
| 2390 | 
            +
              # @param pts [Set<OpenStudio::Point3dVector>] 3d points
         | 
| 2391 | 
            +
              # @param p1 [OpenStudio::Point3d] a 3D point
         | 
| 2392 | 
            +
              #
         | 
| 2393 | 
            +
              # @return [Bool] whether part of a set of 3D points
         | 
| 2394 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2395 | 
            +
              def holds?(pts = nil, p1 = nil)
         | 
| 2396 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2397 | 
            +
                pts = to_p3Dv(pts)
         | 
| 2398 | 
            +
                cl  = OpenStudio::Point3d
         | 
| 2399 | 
            +
                return mismatch("point", p1, cl, mth, DBG, false) unless p1.is_a?(cl)
         | 
| 1032 2400 |  | 
| 1033 | 
            -
             | 
| 1034 | 
            -
                    set = story.defaultConstructionSet.get
         | 
| 1035 | 
            -
                    return set      if holdsConstruction?(set, base, ground, exterior, type)
         | 
| 1036 | 
            -
                  end
         | 
| 1037 | 
            -
                end
         | 
| 2401 | 
            +
                pts.each { |pt| return true if same?(p1, pt) }
         | 
| 1038 2402 |  | 
| 1039 | 
            -
                 | 
| 2403 | 
            +
                false
         | 
| 2404 | 
            +
              end
         | 
| 1040 2405 |  | 
| 1041 | 
            -
             | 
| 1042 | 
            -
             | 
| 1043 | 
            -
             | 
| 2406 | 
            +
              ##
         | 
| 2407 | 
            +
              # Flattens OpenStudio 3D points vs X, Y or Z axes.
         | 
| 2408 | 
            +
              #
         | 
| 2409 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] 3D points
         | 
| 2410 | 
            +
              # @param axs [Symbol] :x, :y or :z axis
         | 
| 2411 | 
            +
              # @param val [#to_f] axis value
         | 
| 2412 | 
            +
              #
         | 
| 2413 | 
            +
              # @return [OpenStudio::Point3dVector] flattened points (see logs if empty)
         | 
| 2414 | 
            +
              def flatten(pts = nil, axs = :z, val = 0)
         | 
| 2415 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2416 | 
            +
                pts = to_p3Dv(pts)
         | 
| 2417 | 
            +
                v   = OpenStudio::Point3dVector.new
         | 
| 2418 | 
            +
                ok1 = val.respond_to?(:to_f)
         | 
| 2419 | 
            +
                ok2 = [:x, :y, :z].include?(axs)
         | 
| 2420 | 
            +
                return mismatch("val", val, Numeric, mth,    DBG, v) unless ok1
         | 
| 2421 | 
            +
                return invalid("axis (XYZ?)",        mth, 2, DBG, v) unless ok2
         | 
| 2422 | 
            +
             | 
| 2423 | 
            +
                val = val.to_f
         | 
| 2424 | 
            +
             | 
| 2425 | 
            +
                case axs
         | 
| 2426 | 
            +
                when :x
         | 
| 2427 | 
            +
                  pts.each { |pt| v << OpenStudio::Point3d.new(val, pt.y, pt.z) }
         | 
| 2428 | 
            +
                when :y
         | 
| 2429 | 
            +
                  pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, val, pt.z) }
         | 
| 2430 | 
            +
                else
         | 
| 2431 | 
            +
                  pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, val) }
         | 
| 1044 2432 | 
             
                end
         | 
| 1045 2433 |  | 
| 1046 | 
            -
                 | 
| 2434 | 
            +
                v
         | 
| 1047 2435 | 
             
              end
         | 
| 1048 2436 |  | 
| 1049 2437 | 
             
              ##
         | 
| 1050 | 
            -
              #  | 
| 2438 | 
            +
              # Returns true if OpenStudio 3D points share X, Y or Z coordinates.
         | 
| 1051 2439 | 
             
              #
         | 
| 1052 | 
            -
              # @param  | 
| 2440 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] OpenStudio 3D points
         | 
| 2441 | 
            +
              # @param axs [Symbol] if potentially along :x, :y or :z axis
         | 
| 2442 | 
            +
              # @param val [Numeric] axis value
         | 
| 1053 2443 | 
             
              #
         | 
| 1054 | 
            -
              # @return [Bool]  | 
| 1055 | 
            -
              # @return [ | 
| 1056 | 
            -
              def  | 
| 2444 | 
            +
              # @return [Bool] if points share X, Y or Z coordinates
         | 
| 2445 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2446 | 
            +
              def xyz?(pts = nil, axs = :z, val = 0)
         | 
| 1057 2447 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1058 | 
            -
                 | 
| 1059 | 
            -
             | 
| 1060 | 
            -
                 | 
| 1061 | 
            -
                return  | 
| 1062 | 
            -
             | 
| 1063 | 
            -
                 | 
| 2448 | 
            +
                pts = to_p3Dv(pts)
         | 
| 2449 | 
            +
                ok1 = val.respond_to?(:to_f)
         | 
| 2450 | 
            +
                ok2 = [:x, :y, :z].include?(axs)
         | 
| 2451 | 
            +
                return false if pts.empty?
         | 
| 2452 | 
            +
                return mismatch("val", val, Numeric, mth,    DBG, false) unless ok1
         | 
| 2453 | 
            +
                return invalid("axis (XYZ?)",        mth, 2, DBG, false) unless ok2
         | 
| 2454 | 
            +
             | 
| 2455 | 
            +
                val = val.to_f
         | 
| 2456 | 
            +
             | 
| 2457 | 
            +
                case axs
         | 
| 2458 | 
            +
                when :x
         | 
| 2459 | 
            +
                  pts.each { |pt| return false if (pt.x - val).abs > TOL }
         | 
| 2460 | 
            +
                when :y
         | 
| 2461 | 
            +
                  pts.each { |pt| return false if (pt.y - val).abs > TOL }
         | 
| 2462 | 
            +
                else
         | 
| 2463 | 
            +
                  pts.each { |pt| return false if (pt.z - val).abs > TOL }
         | 
| 2464 | 
            +
                end
         | 
| 1064 2465 |  | 
| 1065 2466 | 
             
                true
         | 
| 1066 2467 | 
             
              end
         | 
| 1067 2468 |  | 
| 1068 2469 | 
             
              ##
         | 
| 1069 | 
            -
              #  | 
| 2470 | 
            +
              # Returns next sequential point in an OpenStudio 3D point vector.
         | 
| 1070 2471 | 
             
              #
         | 
| 1071 | 
            -
              # @param  | 
| 2472 | 
            +
              # @param pts [OpenStudio::Point3dVector] 3D points
         | 
| 2473 | 
            +
              # @param pt [OpenStudio::Point3d] a given 3D point
         | 
| 1072 2474 | 
             
              #
         | 
| 1073 | 
            -
              # @return [ | 
| 1074 | 
            -
              # @return [ | 
| 1075 | 
            -
              def  | 
| 2475 | 
            +
              # @return [OpenStudio::Point3d] the next sequential point
         | 
| 2476 | 
            +
              # @return [nil] if invalid input (see logs)
         | 
| 2477 | 
            +
              def next(pts = nil, pt = nil)
         | 
| 1076 2478 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1077 | 
            -
                 | 
| 2479 | 
            +
                pts = to_p3Dv(pts)
         | 
| 2480 | 
            +
                cl  = OpenStudio::Point3d
         | 
| 2481 | 
            +
                return mismatch("point", pt, cl, mth)  unless pt.is_a?(cl)
         | 
| 2482 | 
            +
                return invalid("points (2+)", mth, 1, WRN) if pts.size < 2
         | 
| 1078 2483 |  | 
| 1079 | 
            -
                 | 
| 2484 | 
            +
                pair = pts.each_cons(2).find { |p1, _| same?(p1, pt) }
         | 
| 1080 2485 |  | 
| 1081 | 
            -
                 | 
| 1082 | 
            -
                return mismatch(id, lc, cl, mth, DBG, 0.0)         unless lc.is_a?(cl)
         | 
| 1083 | 
            -
             | 
| 1084 | 
            -
                ok = standardOpaqueLayers?(lc)
         | 
| 1085 | 
            -
                log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})")   unless ok
         | 
| 1086 | 
            -
                return 0.0                                                         unless ok
         | 
| 1087 | 
            -
             | 
| 1088 | 
            -
                thickness = 0.0
         | 
| 1089 | 
            -
                lc.layers.each { |m| thickness += m.thickness }
         | 
| 1090 | 
            -
             | 
| 1091 | 
            -
                thickness
         | 
| 2486 | 
            +
                pair.nil? ? pts.first : pair.last
         | 
| 1092 2487 | 
             
              end
         | 
| 1093 2488 |  | 
| 1094 2489 | 
             
              ##
         | 
| 1095 | 
            -
              #  | 
| 2490 | 
            +
              # Returns unique OpenStudio 3D points from an OpenStudio 3D point vector.
         | 
| 1096 2491 | 
             
              #
         | 
| 1097 | 
            -
              # @param  | 
| 2492 | 
            +
              # @param pts [Set<OpenStudio::Point3d] 3D points
         | 
| 2493 | 
            +
              # @param n [#to_i] requested number of unique points (0 returns all)
         | 
| 1098 2494 | 
             
              #
         | 
| 1099 | 
            -
              # @return [ | 
| 1100 | 
            -
              def  | 
| 1101 | 
            -
                # The sum of thermal resistances of calculated exterior and interior film
         | 
| 1102 | 
            -
                # coefficients under standard winter conditions are taken from:
         | 
| 1103 | 
            -
                #
         | 
| 1104 | 
            -
                #   https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
         | 
| 1105 | 
            -
                #   window-calculation-module.html#simple-window-model
         | 
| 1106 | 
            -
                #
         | 
| 1107 | 
            -
                # These remain acceptable approximations for flat windows, yet likely
         | 
| 1108 | 
            -
                # unsuitable for subsurfaces with curved or projecting shapes like domed
         | 
| 1109 | 
            -
                # skylights. The solution here is considered an adequate fix for reporting,
         | 
| 1110 | 
            -
                # awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
         | 
| 1111 | 
            -
                # (or ISO) air film resistances under standard winter conditions.
         | 
| 1112 | 
            -
                #
         | 
| 1113 | 
            -
                # For U-factors above 8.0 W/m2•K (or invalid input), the function returns
         | 
| 1114 | 
            -
                # 0.1216 m2•K/W, which corresponds to a construction with a single glass
         | 
| 1115 | 
            -
                # layer thickness of 2mm & k = ~0.6 W/m.K.
         | 
| 1116 | 
            -
                #
         | 
| 1117 | 
            -
                # The EnergyPlus Engineering calculations were designed for vertical windows
         | 
| 1118 | 
            -
                # - not horizontal, slanted or domed surfaces - use with caution.
         | 
| 2495 | 
            +
              # @return [OpenStudio::Point3dVector] unique points (see logs if empty)
         | 
| 2496 | 
            +
              def getUniques(pts = nil, n = 0)
         | 
| 1119 2497 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1120 | 
            -
                 | 
| 2498 | 
            +
                pts = to_p3Dv(pts)
         | 
| 2499 | 
            +
                ok  = n.respond_to?(:to_i)
         | 
| 2500 | 
            +
                v   = OpenStudio::Point3dVector.new
         | 
| 2501 | 
            +
                return v if pts.empty?
         | 
| 2502 | 
            +
                return mismatch("n unique points", n, Integer, mth, DBG, v) unless ok
         | 
| 1121 2503 |  | 
| 1122 | 
            -
                 | 
| 1123 | 
            -
                return invalid("usi", mth, 1, WRN, 0.1216)             if usi > 8.0
         | 
| 1124 | 
            -
                return negative("usi", mth, WRN, 0.1216)               if usi < 0
         | 
| 1125 | 
            -
                return zero("usi", mth, WRN, 0.1216)                   if usi.abs < TOL
         | 
| 2504 | 
            +
                pts.each { |pt| v << pt unless holds?(v, pt) }
         | 
| 1126 2505 |  | 
| 1127 | 
            -
                 | 
| 2506 | 
            +
                n = n.to_i
         | 
| 2507 | 
            +
                n = 0    unless n.abs < v.size
         | 
| 2508 | 
            +
                v = v[0..n]  if n > 0
         | 
| 2509 | 
            +
                v = v[n..-1] if n < 0
         | 
| 1128 2510 |  | 
| 1129 | 
            -
                 | 
| 1130 | 
            -
                return rsi + 1 / (1.788041 * usi - 2.886625)
         | 
| 2511 | 
            +
                v
         | 
| 1131 2512 | 
             
              end
         | 
| 1132 2513 |  | 
| 1133 2514 | 
             
              ##
         | 
| 1134 | 
            -
              #  | 
| 2515 | 
            +
              # Returns sequential non-collinear points in an OpenStudio 3D point vector.
         | 
| 1135 2516 | 
             
              #
         | 
| 1136 | 
            -
              # @param  | 
| 1137 | 
            -
              # @param  | 
| 1138 | 
            -
              # @param t [Float] gas temperature (°C) (optional)
         | 
| 2517 | 
            +
              # @param pts [Set<OpenStudio::Point3d] 3D points
         | 
| 2518 | 
            +
              # @param n [#to_i] requested number of non-collinears (0 returns all)
         | 
| 1139 2519 | 
             
              #
         | 
| 1140 | 
            -
              # @return [ | 
| 1141 | 
            -
              def  | 
| 1142 | 
            -
                # This is adapted from BTAP's Material Module's "get_conductance" (P. Lopez)
         | 
| 1143 | 
            -
                #
         | 
| 1144 | 
            -
                #   https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
         | 
| 1145 | 
            -
                #   c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
         | 
| 1146 | 
            -
                #   btap_equest_converter/envelope.rb#L122
         | 
| 2520 | 
            +
              # @return [OpenStudio::Point3dVector] non-collinears (see logs if empty)
         | 
| 2521 | 
            +
              def getNonCollinears(pts = nil, n = 0)
         | 
| 1147 2522 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1148 | 
            -
                 | 
| 1149 | 
            -
                 | 
| 1150 | 
            -
             | 
| 1151 | 
            -
                 | 
| 1152 | 
            -
             | 
| 1153 | 
            -
                 | 
| 1154 | 
            -
             | 
| 1155 | 
            -
                 | 
| 1156 | 
            -
                 | 
| 1157 | 
            -
             | 
| 1158 | 
            -
             | 
| 1159 | 
            -
             | 
| 1160 | 
            -
             | 
| 1161 | 
            -
             | 
| 1162 | 
            -
             | 
| 1163 | 
            -
             | 
| 1164 | 
            -
             | 
| 1165 | 
            -
             | 
| 1166 | 
            -
                   | 
| 1167 | 
            -
             | 
| 1168 | 
            -
                  return 1 / m.to_SimpleGlazing.get.uFactor                     unless empty
         | 
| 1169 | 
            -
             | 
| 1170 | 
            -
                  empty = m.to_StandardGlazing.empty?
         | 
| 1171 | 
            -
                  rsi += m.to_StandardGlazing.get.thermalResistance             unless empty
         | 
| 1172 | 
            -
                  empty = m.to_RefractionExtinctionGlazing.empty?
         | 
| 1173 | 
            -
                  rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty
         | 
| 1174 | 
            -
                  empty = m.to_Gas.empty?
         | 
| 1175 | 
            -
                  rsi += m.to_Gas.get.getThermalResistance(t)                   unless empty
         | 
| 1176 | 
            -
                  empty = m.to_GasMixture.empty?
         | 
| 1177 | 
            -
                  rsi += m.to_GasMixture.get.getThermalResistance(t)            unless empty
         | 
| 2523 | 
            +
                pts = getUniques(pts)
         | 
| 2524 | 
            +
                ok  = n.respond_to?(:to_i)
         | 
| 2525 | 
            +
                v   = OpenStudio::Point3dVector.new
         | 
| 2526 | 
            +
                a   = []
         | 
| 2527 | 
            +
                return pts if pts.size < 2
         | 
| 2528 | 
            +
                return mismatch("n non-collinears", n, Integer, mth, DBG, v) unless ok
         | 
| 2529 | 
            +
             | 
| 2530 | 
            +
                # Evaluate cross product of vectors of 3x sequential points.
         | 
| 2531 | 
            +
                pts.each_with_index do |p2, i2|
         | 
| 2532 | 
            +
                  i1  = i2 - 1
         | 
| 2533 | 
            +
                  i3  = i2 + 1
         | 
| 2534 | 
            +
                  i3  = 0 if i3 == pts.size
         | 
| 2535 | 
            +
                  p1  = pts[i1]
         | 
| 2536 | 
            +
                  p3  = pts[i3]
         | 
| 2537 | 
            +
                  v13 = p3 - p1
         | 
| 2538 | 
            +
                  v12 = p2 - p1
         | 
| 2539 | 
            +
                  next if v12.cross(v13).length < TOL
         | 
| 2540 | 
            +
             | 
| 2541 | 
            +
                  a << p2
         | 
| 2542 | 
            +
                end
         | 
| 1178 2543 |  | 
| 1179 | 
            -
             | 
| 1180 | 
            -
                   | 
| 1181 | 
            -
                  rsi += m.to_StandardOpaqueMaterial.get.thermalResistance      unless empty
         | 
| 1182 | 
            -
                  empty = m.to_MasslessOpaqueMaterial.empty?
         | 
| 1183 | 
            -
                  rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance      unless empty
         | 
| 1184 | 
            -
                  empty = m.to_RoofVegetation.empty?
         | 
| 1185 | 
            -
                  rsi += m.to_RoofVegetation.get.thermalResistance              unless empty
         | 
| 1186 | 
            -
                  empty = m.to_AirGap.empty?
         | 
| 1187 | 
            -
                  rsi += m.to_AirGap.get.thermalResistance                      unless empty
         | 
| 2544 | 
            +
                if holds?(a, pts[0])
         | 
| 2545 | 
            +
                  a = a.rotate(-1) unless same?(a[0], pts[0])
         | 
| 1188 2546 | 
             
                end
         | 
| 1189 2547 |  | 
| 1190 | 
            -
                 | 
| 2548 | 
            +
                n = n.to_i
         | 
| 2549 | 
            +
                n = 0    unless n.abs < pts.size
         | 
| 2550 | 
            +
                a = a[0..n]  if n > 0
         | 
| 2551 | 
            +
                a = a[n..-1] if n < 0
         | 
| 2552 | 
            +
             | 
| 2553 | 
            +
                to_p3Dv(a)
         | 
| 1191 2554 | 
             
              end
         | 
| 1192 2555 |  | 
| 1193 2556 | 
             
              ##
         | 
| 1194 | 
            -
              #  | 
| 1195 | 
            -
              #  | 
| 1196 | 
            -
              #  | 
| 1197 | 
            -
              #  | 
| 2557 | 
            +
              # Returns paired sequential points as (non-zero length) line segments. If the
         | 
| 2558 | 
            +
              # set strictly holds 2x unique points, a single segment is returned.
         | 
| 2559 | 
            +
              # Otherwise, the returned number of segments equals the number of unique
         | 
| 2560 | 
            +
              # points. If non-collinearity is requested, then the number of returned
         | 
| 2561 | 
            +
              # segments equals the number of non-colliear points.
         | 
| 1198 2562 | 
             
              #
         | 
| 1199 | 
            -
              # @param  | 
| 2563 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] 3D points
         | 
| 2564 | 
            +
              # @param co [Bool] whether to keep collinear points
         | 
| 1200 2565 | 
             
              #
         | 
| 1201 | 
            -
              # @return [ | 
| 1202 | 
            -
               | 
| 1203 | 
            -
              def insulatingLayer(lc = nil)
         | 
| 2566 | 
            +
              # @return [OpenStudio::Point3dVectorVector] line segments (see logs if empty)
         | 
| 2567 | 
            +
              def getSegments(pts = nil, co = false)
         | 
| 1204 2568 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1205 | 
            -
                 | 
| 1206 | 
            -
                 | 
| 1207 | 
            -
                 | 
| 1208 | 
            -
             | 
| 1209 | 
            -
                return  | 
| 1210 | 
            -
             | 
| 1211 | 
            -
                 | 
| 1212 | 
            -
             | 
| 1213 | 
            -
             | 
| 1214 | 
            -
             | 
| 1215 | 
            -
             | 
| 1216 | 
            -
             | 
| 1217 | 
            -
             | 
| 1218 | 
            -
             | 
| 1219 | 
            -
             | 
| 1220 | 
            -
             | 
| 1221 | 
            -
                    else
         | 
| 1222 | 
            -
                      res[:r    ] = m.thermalResistance
         | 
| 1223 | 
            -
                      res[:index] = i
         | 
| 1224 | 
            -
                      res[:type ] = :massless
         | 
| 1225 | 
            -
                    end
         | 
| 1226 | 
            -
                  end
         | 
| 1227 | 
            -
             | 
| 1228 | 
            -
                  unless m.to_StandardOpaqueMaterial.empty?
         | 
| 1229 | 
            -
                    m             = m.to_StandardOpaqueMaterial.get
         | 
| 1230 | 
            -
                    k             = m.thermalConductivity
         | 
| 1231 | 
            -
                    d             = m.thickness
         | 
| 1232 | 
            -
             | 
| 1233 | 
            -
                    if d < 0.003 || k > 3.0 || d / k < res[:r]
         | 
| 1234 | 
            -
                      i += 1
         | 
| 1235 | 
            -
                      next
         | 
| 1236 | 
            -
                    else
         | 
| 1237 | 
            -
                      res[:r    ] = d / k
         | 
| 1238 | 
            -
                      res[:index] = i
         | 
| 1239 | 
            -
                      res[:type ] = :standard
         | 
| 1240 | 
            -
                    end
         | 
| 1241 | 
            -
                  end
         | 
| 1242 | 
            -
             | 
| 1243 | 
            -
                  i += 1
         | 
| 2569 | 
            +
                vv  = OpenStudio::Point3dVectorVector.new
         | 
| 2570 | 
            +
                co  = false                 unless [true, false].include?(co)
         | 
| 2571 | 
            +
                pts = getNonCollinears(pts) unless co
         | 
| 2572 | 
            +
                pts = getUniques(pts)           if co
         | 
| 2573 | 
            +
                return vv                       if pts.size < 2
         | 
| 2574 | 
            +
             | 
| 2575 | 
            +
                pts.each_with_index do |p1, i1|
         | 
| 2576 | 
            +
                  i2 = i1 + 1
         | 
| 2577 | 
            +
                  i2 = 0 if i2 == pts.size
         | 
| 2578 | 
            +
                  p2 = pts[i2]
         | 
| 2579 | 
            +
             | 
| 2580 | 
            +
                  line = OpenStudio::Point3dVector.new
         | 
| 2581 | 
            +
                  line << p1
         | 
| 2582 | 
            +
                  line << p2
         | 
| 2583 | 
            +
                  vv << line
         | 
| 2584 | 
            +
                  break if pts.size == 2
         | 
| 1244 2585 | 
             
                end
         | 
| 1245 2586 |  | 
| 1246 | 
            -
                 | 
| 2587 | 
            +
                vv
         | 
| 1247 2588 | 
             
              end
         | 
| 1248 2589 |  | 
| 1249 2590 | 
             
              ##
         | 
| 1250 | 
            -
              #  | 
| 2591 | 
            +
              # Returns points as (non-zero length) 'triads', i.e. 3x sequential points.
         | 
| 2592 | 
            +
              # If the set holds less than 3x unique points, an empty triad is
         | 
| 2593 | 
            +
              # returned. Otherwise, the returned number of triads equals the number of
         | 
| 2594 | 
            +
              # unique points. If non-collinearity is requested, then the number of
         | 
| 2595 | 
            +
              # returned triads equals the number of non-collinear points.
         | 
| 1251 2596 | 
             
              #
         | 
| 1252 | 
            -
              # @param  | 
| 1253 | 
            -
              # @param  | 
| 2597 | 
            +
              # @param pts [OpenStudio::Point3dVector] 3D points
         | 
| 2598 | 
            +
              # @param co [Bool] whether to keep collinear points
         | 
| 1254 2599 | 
             
              #
         | 
| 1255 | 
            -
              # @return [ | 
| 1256 | 
            -
               | 
| 1257 | 
            -
              def transforms(model = nil, group = nil)
         | 
| 2600 | 
            +
              # @return [OpenStudio::Point3dVectorVector] triads (see logs if empty)
         | 
| 2601 | 
            +
              def getTriads(pts = nil, co = false)
         | 
| 1258 2602 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1259 | 
            -
                 | 
| 1260 | 
            -
                 | 
| 1261 | 
            -
                 | 
| 1262 | 
            -
             | 
| 1263 | 
            -
                return  | 
| 1264 | 
            -
             | 
| 1265 | 
            -
             | 
| 1266 | 
            -
             | 
| 1267 | 
            -
             | 
| 1268 | 
            -
             | 
| 1269 | 
            -
             | 
| 1270 | 
            -
             | 
| 2603 | 
            +
                vv  = OpenStudio::Point3dVectorVector.new
         | 
| 2604 | 
            +
                co  = false                 unless [true, false].include?(co)
         | 
| 2605 | 
            +
                pts = getNonCollinears(pts) unless co
         | 
| 2606 | 
            +
                pts = getUniques(pts)           if co
         | 
| 2607 | 
            +
                return vv                       if pts.size < 2
         | 
| 2608 | 
            +
             | 
| 2609 | 
            +
                pts.each_with_index do |p1, i1|
         | 
| 2610 | 
            +
                  i2 = i1 + 1
         | 
| 2611 | 
            +
                  i2 = 0 if i2 == pts.size
         | 
| 2612 | 
            +
                  i3 = i2 + 1
         | 
| 2613 | 
            +
                  i3 = 0 if i3 == pts.size
         | 
| 2614 | 
            +
                  p2 = pts[i2]
         | 
| 2615 | 
            +
                  p3 = pts[i3]
         | 
| 2616 | 
            +
             | 
| 2617 | 
            +
                  tri = OpenStudio::Point3dVector.new
         | 
| 2618 | 
            +
                  tri << p1
         | 
| 2619 | 
            +
                  tri << p2
         | 
| 2620 | 
            +
                  tri << p3
         | 
| 2621 | 
            +
                  vv << tri
         | 
| 2622 | 
            +
                end
         | 
| 1271 2623 |  | 
| 1272 | 
            -
                 | 
| 2624 | 
            +
                vv
         | 
| 1273 2625 | 
             
              end
         | 
| 1274 2626 |  | 
| 1275 2627 | 
             
              ##
         | 
| 1276 | 
            -
              #  | 
| 2628 | 
            +
              # Determines if pre-'aligned' OpenStudio 3D points are listed clockwise.
         | 
| 1277 2629 | 
             
              #
         | 
| 1278 | 
            -
              # @param  | 
| 1279 | 
            -
              # @param m [Float] a scalar
         | 
| 2630 | 
            +
              # @param pts [OpenStudio::Point3dVector] 3D points
         | 
| 1280 2631 | 
             
              #
         | 
| 1281 | 
            -
              # @return [ | 
| 1282 | 
            -
              # @return [ | 
| 1283 | 
            -
              def  | 
| 2632 | 
            +
              # @return [Bool] whether sequence is clockwise
         | 
| 2633 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2634 | 
            +
              def clockwise?(pts = nil)
         | 
| 1284 2635 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1285 | 
            -
                 | 
| 1286 | 
            -
                 | 
| 2636 | 
            +
                pts = to_p3Dv(pts)
         | 
| 2637 | 
            +
                n   = false
         | 
| 2638 | 
            +
                return invalid("points (3+)",      mth, 1, DBG, n)     if pts.size < 3
         | 
| 2639 | 
            +
                return invalid("points (aligned)", mth, 1, DBG, n) unless xyz?(pts, :z, 0)
         | 
| 1287 2640 |  | 
| 1288 | 
            -
                 | 
| 1289 | 
            -
                return mismatch("x",    v.x, cl2, mth, DBG, v) unless v.x.respond_to?(:to_f)
         | 
| 1290 | 
            -
                return mismatch("y",    v.y, cl2, mth, DBG, v) unless v.y.respond_to?(:to_f)
         | 
| 1291 | 
            -
                return mismatch("z",    v.z, cl2, mth, DBG, v) unless v.z.respond_to?(:to_f)
         | 
| 1292 | 
            -
                return mismatch("m",      m, cl2, mth, DBG, v) unless m.respond_to?(:to_f)
         | 
| 1293 | 
            -
             | 
| 1294 | 
            -
                OpenStudio::Vector3d.new(m * v.x, m * v.y, m * v.z)
         | 
| 2641 | 
            +
                OpenStudio.pointInPolygon(pts.first, pts, TOL)
         | 
| 1295 2642 | 
             
              end
         | 
| 1296 2643 |  | 
| 1297 2644 | 
             
              ##
         | 
| 1298 | 
            -
              #  | 
| 2645 | 
            +
              # Returns 'aligned' OpenStudio 3D points conforming to Openstudio's
         | 
| 2646 | 
            +
              # counterclockwise UpperLeftCorner (ULC) convention.
         | 
| 1299 2647 | 
             
              #
         | 
| 1300 | 
            -
              # @param pts [ | 
| 2648 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] aligned 3D points
         | 
| 1301 2649 | 
             
              #
         | 
| 1302 | 
            -
              # @return [ | 
| 1303 | 
            -
              def  | 
| 2650 | 
            +
              # @return [OpenStudio::Point3dVector] ULC points (see logs if empty)
         | 
| 2651 | 
            +
              def ulc(pts = nil)
         | 
| 1304 2652 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1305 | 
            -
                 | 
| 1306 | 
            -
                cl2 = OpenStudio::Point3d
         | 
| 2653 | 
            +
                pts = to_p3Dv(pts)
         | 
| 1307 2654 | 
             
                v   = OpenStudio::Point3dVector.new
         | 
| 2655 | 
            +
                p0  = OpenStudio::Point3d.new(0,0,0)
         | 
| 2656 | 
            +
                i0  = nil
         | 
| 1308 2657 |  | 
| 1309 | 
            -
                 | 
| 1310 | 
            -
                return  | 
| 2658 | 
            +
                return invalid("points (3+)",      mth, 1, DBG, v)     if pts.size < 3
         | 
| 2659 | 
            +
                return invalid("points (aligned)", mth, 1, DBG, v) unless xyz?(pts, :z, 0)
         | 
| 1311 2660 |  | 
| 1312 | 
            -
                 | 
| 1313 | 
            -
                pts | 
| 2661 | 
            +
                # Ensure counterclockwise sequence.
         | 
| 2662 | 
            +
                pts = pts.to_a
         | 
| 2663 | 
            +
                pts = pts.reverse if clockwise?(pts)
         | 
| 1314 2664 |  | 
| 1315 | 
            -
                 | 
| 2665 | 
            +
                # Fetch index of candidate (0,0,0) point (i == 1, in most cases). Resort
         | 
| 2666 | 
            +
                # to last X == 0 point. Leave as is if failed attempts.
         | 
| 2667 | 
            +
                i0 = pts.index  { |pt| same?(pt, p0) }
         | 
| 2668 | 
            +
                i0 = pts.rindex { |pt| pt.x.abs < TOL } if i0.nil?
         | 
| 2669 | 
            +
             | 
| 2670 | 
            +
                unless i0.nil?
         | 
| 2671 | 
            +
                  i   = pts.size - 1
         | 
| 2672 | 
            +
                  i   = i0 - 1 unless i0 == 0
         | 
| 2673 | 
            +
                  pts = pts.rotate(i)
         | 
| 2674 | 
            +
                end
         | 
| 2675 | 
            +
             | 
| 2676 | 
            +
                to_p3Dv(pts)
         | 
| 1316 2677 | 
             
              end
         | 
| 1317 2678 |  | 
| 1318 2679 | 
             
              ##
         | 
| 1319 | 
            -
              #  | 
| 1320 | 
            -
              #
         | 
| 1321 | 
            -
              #  | 
| 1322 | 
            -
              #  | 
| 1323 | 
            -
              #  | 
| 1324 | 
            -
              #  | 
| 1325 | 
            -
              #
         | 
| 1326 | 
            -
              # @ | 
| 1327 | 
            -
              # @ | 
| 1328 | 
            -
               | 
| 2680 | 
            +
              # Returns an OpenStudio 3D point vector as basis for a valid OpenStudio 3D
         | 
| 2681 | 
            +
              # polygon. In addition to basic OpenStudio polygon tests (e.g. all points
         | 
| 2682 | 
            +
              # sharing the same 3D plane, non-self-intersecting), the method can
         | 
| 2683 | 
            +
              # optionally check for convexity, or ensure uniqueness and/or collinearity.
         | 
| 2684 | 
            +
              # Returned vector can also be 'aligned', as well as in UpperLeftCorner (ULC)
         | 
| 2685 | 
            +
              # counterclockwise sequence, or in clockwise sequence.
         | 
| 2686 | 
            +
              #
         | 
| 2687 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] 3D points
         | 
| 2688 | 
            +
              # @param vx [Bool] whether to check for convexity
         | 
| 2689 | 
            +
              # @param uq [Bool] whether to ensure uniqueness
         | 
| 2690 | 
            +
              # @param co [Bool] whether to ensure non-collinearity
         | 
| 2691 | 
            +
              # @param tt [Bool, OpenStudio::Transformation] whether to 'align'
         | 
| 2692 | 
            +
              # @param sq [:no, :ulc, :cw] unaltered, ULC or clockwise sequence
         | 
| 2693 | 
            +
              #
         | 
| 2694 | 
            +
              # @return [OpenStudio::Point3dVector] 3D points (see logs if empty)
         | 
| 2695 | 
            +
              def poly(pts = nil, vx = false, uq = false, co = true, tt = false, sq = :no)
         | 
| 1329 2696 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1330 | 
            -
                 | 
| 1331 | 
            -
                 | 
| 1332 | 
            -
                 | 
| 2697 | 
            +
                pts = to_p3Dv(pts)
         | 
| 2698 | 
            +
                cl  = OpenStudio::Transformation
         | 
| 2699 | 
            +
                v   = OpenStudio::Point3dVector.new
         | 
| 2700 | 
            +
                vx  = false unless [true, false].include?(vx)
         | 
| 2701 | 
            +
                uq  = false unless [true, false].include?(uq)
         | 
| 2702 | 
            +
                co  = true  unless [true, false].include?(co)
         | 
| 1333 2703 |  | 
| 1334 | 
            -
                 | 
| 1335 | 
            -
                 | 
| 2704 | 
            +
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 2705 | 
            +
                # Exit if mismatched/invalid arguments.
         | 
| 2706 | 
            +
                ok1 = tt == true || tt == false || tt.is_a?(cl)
         | 
| 2707 | 
            +
                ok2 = sq == :no  || sq == :ulc  || sq == :cw
         | 
| 2708 | 
            +
                return invalid("transformation", mth, 5, DBG, v) unless ok1
         | 
| 2709 | 
            +
                return invalid("sequence",       mth, 6, DBG, v) unless ok2
         | 
| 1336 2710 |  | 
| 1337 | 
            -
                 | 
| 1338 | 
            -
                 | 
| 1339 | 
            -
                 | 
| 1340 | 
            -
                 | 
| 2711 | 
            +
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 2712 | 
            +
                # Basic tests:
         | 
| 2713 | 
            +
                p3 = getNonCollinears(pts, 3)
         | 
| 2714 | 
            +
                return empty("polygon", mth, ERR, v) if p3.size < 3
         | 
| 1341 2715 |  | 
| 1342 | 
            -
                 | 
| 1343 | 
            -
                valid2 = p2.is_a?(cl1) || p2.is_a?(Array)
         | 
| 2716 | 
            +
                pln = OpenStudio::Plane.new(p3)
         | 
| 1344 2717 |  | 
| 1345 | 
            -
                 | 
| 1346 | 
            -
             | 
| 1347 | 
            -
                 | 
| 1348 | 
            -
                return empty(i2, mth, ERR, a)                 if p2.empty?
         | 
| 2718 | 
            +
                pts.each do |pt|
         | 
| 2719 | 
            +
                  return empty("plane", mth, ERR, v) unless pln.pointOnPlane(pt)
         | 
| 2720 | 
            +
                end
         | 
| 1349 2721 |  | 
| 1350 | 
            -
                 | 
| 1351 | 
            -
                 | 
| 2722 | 
            +
                t = tt
         | 
| 2723 | 
            +
                t = OpenStudio::Transformation.alignFace(pts) unless tt.is_a?(cl)
         | 
| 2724 | 
            +
                a = (t.inverse * pts).reverse
         | 
| 1352 2725 |  | 
| 1353 | 
            -
                 | 
| 1354 | 
            -
             | 
| 1355 | 
            -
             | 
| 1356 | 
            -
             | 
| 2726 | 
            +
                if tt.is_a?(cl)
         | 
| 2727 | 
            +
                  # Using a transformation that is most likely not specific to pts. The
         | 
| 2728 | 
            +
                  # most probable reason to retain this option is when testing for polygon
         | 
| 2729 | 
            +
                  # intersections, unions, etc., operations that typically require that
         | 
| 2730 | 
            +
                  # points remain nonetheless 'aligned'. If re-activated, this logs a
         | 
| 2731 | 
            +
                  # warning if aligned points aren't @Z =0, before 'flattening'.
         | 
| 2732 | 
            +
                  #
         | 
| 2733 | 
            +
                  #   invalid("points (non-aligned)", mth, 1, WRN) unless xyz?(a, :z, 0)
         | 
| 2734 | 
            +
                  a = flatten(a).to_a unless xyz?(a, :z, 0)
         | 
| 2735 | 
            +
                end
         | 
| 1357 2736 |  | 
| 1358 | 
            -
                 | 
| 1359 | 
            -
                 | 
| 1360 | 
            -
                 | 
| 1361 | 
            -
                 | 
| 1362 | 
            -
                return | 
| 2737 | 
            +
                # The following 2x lines are commented out. This is a very commnon and very
         | 
| 2738 | 
            +
                # useful test, yet tested cases are first caught by the 'pointOnPlane'
         | 
| 2739 | 
            +
                # test above. Keeping it for possible further testing.
         | 
| 2740 | 
            +
                # bad = OpenStudio.selfIntersects(a, TOL)
         | 
| 2741 | 
            +
                # return invalid("points (intersecting)", mth, 1, ERR, v) if bad
         | 
| 1363 2742 |  | 
| 1364 | 
            -
                 | 
| 1365 | 
            -
                 | 
| 1366 | 
            -
                 | 
| 1367 | 
            -
                 | 
| 2743 | 
            +
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 2744 | 
            +
                # Ensure uniqueness and/or non-collinearity. Preserve original sequence.
         | 
| 2745 | 
            +
                p0 = a.first
         | 
| 2746 | 
            +
                a  = OpenStudio.simplify(a, false, TOL)     if uq
         | 
| 2747 | 
            +
                a  = OpenStudio.simplify(a, true,  TOL) unless co
         | 
| 2748 | 
            +
                i0 = a.index { |pt| same?(pt, p0) }
         | 
| 2749 | 
            +
                a  = a.rotate(i0)                       unless i0.nil?
         | 
| 1368 2750 |  | 
| 1369 | 
            -
                 | 
| 1370 | 
            -
                 | 
| 1371 | 
            -
                 | 
| 1372 | 
            -
             | 
| 2751 | 
            +
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 2752 | 
            +
                # Check for convexity (optional).
         | 
| 2753 | 
            +
                if vx
         | 
| 2754 | 
            +
                  a1 = OpenStudio.simplify(a, true, TOL).reverse
         | 
| 2755 | 
            +
                  dX = a1.max_by(&:x).x.abs
         | 
| 2756 | 
            +
                  dY = a1.max_by(&:y).y.abs
         | 
| 2757 | 
            +
                  d  = [dX, dY].max
         | 
| 2758 | 
            +
                  return false if d < TOL
         | 
| 2759 | 
            +
             | 
| 2760 | 
            +
                  u = OpenStudio::Vector3d.new(0, 0, d)
         | 
| 2761 | 
            +
             | 
| 2762 | 
            +
                  a1.each_with_index do |p1, i1|
         | 
| 2763 | 
            +
                    i2 = i1 + 1
         | 
| 2764 | 
            +
                    i2 = 0 if i2 == a1.size
         | 
| 2765 | 
            +
                    p2 = a1[i2]
         | 
| 2766 | 
            +
                    pi = p1 + u
         | 
| 2767 | 
            +
                    vi = OpenStudio::Point3dVector.new
         | 
| 2768 | 
            +
                    vi << pi
         | 
| 2769 | 
            +
                    vi << p1
         | 
| 2770 | 
            +
                    vi << p2
         | 
| 2771 | 
            +
                    plane  = OpenStudio::Plane.new(vi)
         | 
| 2772 | 
            +
                    normal = plane.outwardNormal
         | 
| 2773 | 
            +
             | 
| 2774 | 
            +
                    a1.each do |p3|
         | 
| 2775 | 
            +
                      next if same?(p1, p3)
         | 
| 2776 | 
            +
                      next if same?(p2, p3)
         | 
| 2777 | 
            +
                      next if plane.pointOnPlane(p3)
         | 
| 2778 | 
            +
                      next if normal.dot(p3 - p1) < 0
         | 
| 2779 | 
            +
             | 
| 2780 | 
            +
                      return invalid("points (non-convex)", mth, 1, ERR, v)
         | 
| 2781 | 
            +
                    end
         | 
| 2782 | 
            +
                  end
         | 
| 2783 | 
            +
                end
         | 
| 1373 2784 |  | 
| 1374 | 
            -
                 | 
| 1375 | 
            -
                 | 
| 1376 | 
            -
                 | 
| 2785 | 
            +
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 2786 | 
            +
                # Alter sequence (optional).
         | 
| 2787 | 
            +
                unless tt
         | 
| 2788 | 
            +
                  case sq
         | 
| 2789 | 
            +
                  when :ulc
         | 
| 2790 | 
            +
                    a = to_p3Dv(t * ulc(a.reverse))
         | 
| 2791 | 
            +
                  when :cw
         | 
| 2792 | 
            +
                    a = to_p3Dv(t * a)
         | 
| 2793 | 
            +
                    a = OpenStudio.reverse(a) unless clockwise?(a)
         | 
| 2794 | 
            +
                  else
         | 
| 2795 | 
            +
                    a = to_p3Dv(t * a.reverse)
         | 
| 2796 | 
            +
                  end
         | 
| 2797 | 
            +
                else
         | 
| 2798 | 
            +
                  case sq
         | 
| 2799 | 
            +
                  when :ulc
         | 
| 2800 | 
            +
                    a = ulc(a.reverse)
         | 
| 2801 | 
            +
                  when :cw
         | 
| 2802 | 
            +
                    a = to_p3Dv(a)
         | 
| 2803 | 
            +
                    a = OpenStudio.reverse(a) unless clockwise?(a)
         | 
| 2804 | 
            +
                  else
         | 
| 2805 | 
            +
                    a = to_p3Dv(a.reverse)
         | 
| 2806 | 
            +
                  end
         | 
| 2807 | 
            +
                end
         | 
| 1377 2808 |  | 
| 1378 | 
            -
                 | 
| 2809 | 
            +
                a
         | 
| 2810 | 
            +
              end
         | 
| 1379 2811 |  | 
| 1380 | 
            -
             | 
| 1381 | 
            -
             | 
| 1382 | 
            -
             | 
| 2812 | 
            +
              ##
         | 
| 2813 | 
            +
              # Returns 'width' of a set of OpenStudio 3D points (perpendicular view).
         | 
| 2814 | 
            +
              #
         | 
| 2815 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] 3D points
         | 
| 2816 | 
            +
              #
         | 
| 2817 | 
            +
              # @return [Float] left-to-right width
         | 
| 2818 | 
            +
              # @return [0.0] if invalid inputs (see logs)
         | 
| 2819 | 
            +
              def width(pts = nil)
         | 
| 2820 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 1383 2821 |  | 
| 1384 | 
            -
                true
         | 
| 2822 | 
            +
                poly(pts, false, true, false, true).max_by(&:x).x
         | 
| 1385 2823 | 
             
              end
         | 
| 1386 2824 |  | 
| 1387 2825 | 
             
              ##
         | 
| 1388 | 
            -
              #  | 
| 2826 | 
            +
              # Returns 'height' of a set of OpenStudio 3D points (perpendicular view).
         | 
| 1389 2827 | 
             
              #
         | 
| 1390 | 
            -
              # @param  | 
| 1391 | 
            -
              # @param p2 [OpenStudio::Point3dVector] or Point3D array of polygon #2
         | 
| 1392 | 
            -
              # @param id1 [String] polygon #1 identifier (optional)
         | 
| 1393 | 
            -
              # @param id2 [String] polygon #2 identifier (optional)
         | 
| 2828 | 
            +
              # @param pts [Set<OpenStudio::Point3d>] 3D points
         | 
| 1394 2829 | 
             
              #
         | 
| 1395 | 
            -
              # @return  | 
| 1396 | 
            -
              # @return [ | 
| 1397 | 
            -
              def  | 
| 2830 | 
            +
              # @return [Float] top-to-bottom height
         | 
| 2831 | 
            +
              # @return [0.0] if invalid inputs (see logs)
         | 
| 2832 | 
            +
              def height(pts = nil)
         | 
| 1398 2833 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1399 | 
            -
             | 
| 1400 | 
            -
                 | 
| 1401 | 
            -
             | 
| 1402 | 
            -
             | 
| 1403 | 
            -
             | 
| 1404 | 
            -
             | 
| 1405 | 
            -
             | 
| 1406 | 
            -
             | 
| 1407 | 
            -
             | 
| 1408 | 
            -
             | 
| 1409 | 
            -
             | 
| 1410 | 
            -
             | 
| 1411 | 
            -
             | 
| 1412 | 
            -
             | 
| 1413 | 
            -
             | 
| 1414 | 
            -
                 | 
| 1415 | 
            -
                 | 
| 1416 | 
            -
                 | 
| 1417 | 
            -
                return  | 
| 1418 | 
            -
             | 
| 1419 | 
            -
             | 
| 1420 | 
            -
                 | 
| 1421 | 
            -
             | 
| 1422 | 
            -
                 | 
| 1423 | 
            -
                 | 
| 1424 | 
            -
                 | 
| 1425 | 
            -
                 | 
| 1426 | 
            -
                return | 
| 1427 | 
            -
                return | 
| 1428 | 
            -
             | 
| 1429 | 
            -
                 | 
| 1430 | 
            -
                 | 
| 1431 | 
            -
                 | 
| 1432 | 
            -
                return | 
| 1433 | 
            -
                return  false                                                if ft_p2.empty?
         | 
| 1434 | 
            -
             | 
| 1435 | 
            -
                area1 = OpenStudio.getArea(ft_p1)
         | 
| 1436 | 
            -
                area2 = OpenStudio.getArea(ft_p2)
         | 
| 1437 | 
            -
                return  empty("#{i1} area", mth, ERR, a)                     if area1.empty?
         | 
| 1438 | 
            -
                return  empty("#{i2} area", mth, ERR, a)                     if area2.empty?
         | 
| 2834 | 
            +
             | 
| 2835 | 
            +
                poly(pts, false, true, false, true).max_by(&:y).y
         | 
| 2836 | 
            +
              end
         | 
| 2837 | 
            +
             | 
| 2838 | 
            +
              ##
         | 
| 2839 | 
            +
              # Determines whether a 1st OpenStudio polygon fits in a 2nd polygon.
         | 
| 2840 | 
            +
              #
         | 
| 2841 | 
            +
              # @param p1 [Set<OpenStudio::Point3d>] 1st set of 3D points
         | 
| 2842 | 
            +
              # @param p2 [Set<OpenStudio::Point3d>] 2nd set of 3D points
         | 
| 2843 | 
            +
              # @param flat [Bool] whether points are to be pre-flattened (Z=0)
         | 
| 2844 | 
            +
              #
         | 
| 2845 | 
            +
              # @return [Bool] whether 1st polygon fits within the 2nd polygon
         | 
| 2846 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2847 | 
            +
              def fits?(p1 = nil, p2 = nil, flat = true)
         | 
| 2848 | 
            +
                mth  = "OSut::#{__callee__}"
         | 
| 2849 | 
            +
                p1   = poly(p1, false, true, false)
         | 
| 2850 | 
            +
                p2   = poly(p2, false, true, false)
         | 
| 2851 | 
            +
                flat = true  unless [true, false].include?(flat)
         | 
| 2852 | 
            +
                return false     if p1.empty?
         | 
| 2853 | 
            +
                return false     if p2.empty?
         | 
| 2854 | 
            +
             | 
| 2855 | 
            +
                # Aligned, clockwise points using transformation from 2nd polygon.
         | 
| 2856 | 
            +
                t  = OpenStudio::Transformation.alignFace(p2)
         | 
| 2857 | 
            +
                p1 = poly(p1, false, false, true, t, :cw)
         | 
| 2858 | 
            +
                p2 = poly(p2, false, false, true, t, :cw)
         | 
| 2859 | 
            +
                p1 = flatten(p1) if flat
         | 
| 2860 | 
            +
                p2 = flatten(p2) if flat
         | 
| 2861 | 
            +
                return false     if p1.empty?
         | 
| 2862 | 
            +
                return false     if p2.empty?
         | 
| 2863 | 
            +
             | 
| 2864 | 
            +
                area1 = OpenStudio.getArea(p1)
         | 
| 2865 | 
            +
                area2 = OpenStudio.getArea(p2)
         | 
| 2866 | 
            +
                return empty("points 1 area", mth, ERR, false) if area1.empty?
         | 
| 2867 | 
            +
                return empty("points 2 area", mth, ERR, false) if area2.empty?
         | 
| 1439 2868 |  | 
| 1440 2869 | 
             
                area1 = area1.get
         | 
| 1441 2870 | 
             
                area2 = area2.get
         | 
| 1442 | 
            -
                union = OpenStudio.join( | 
| 1443 | 
            -
                return | 
| 2871 | 
            +
                union = OpenStudio.join(p1, p2, TOL2)
         | 
| 2872 | 
            +
                return false if union.empty?
         | 
| 1444 2873 |  | 
| 1445 2874 | 
             
                union = union.get
         | 
| 1446 2875 | 
             
                area  = OpenStudio.getArea(union)
         | 
| 1447 | 
            -
                return  | 
| 2876 | 
            +
                return false if area.empty?
         | 
| 1448 2877 |  | 
| 1449 2878 | 
             
                area = area.get
         | 
| 1450 | 
            -
                return false                                                 if area < TOL
         | 
| 1451 2879 |  | 
| 1452 | 
            -
                 | 
| 1453 | 
            -
             | 
| 2880 | 
            +
                if area > TOL
         | 
| 2881 | 
            +
                  return true if (area - area2).abs < TOL
         | 
| 2882 | 
            +
                end
         | 
| 1454 2883 |  | 
| 1455 | 
            -
                 | 
| 2884 | 
            +
                false
         | 
| 1456 2885 | 
             
              end
         | 
| 1457 2886 |  | 
| 1458 2887 | 
             
              ##
         | 
| 1459 | 
            -
              #  | 
| 2888 | 
            +
              # Determines whether OpenStudio polygons overlap.
         | 
| 1460 2889 | 
             
              #
         | 
| 1461 | 
            -
              # @param p1 [OpenStudio:: | 
| 1462 | 
            -
              # @param  | 
| 1463 | 
            -
              # @param  | 
| 2890 | 
            +
              # @param p1 [Set<OpenStudio::Point3d>] 1st set of 3D points
         | 
| 2891 | 
            +
              # @param p2 [Set<OpenStudio::Point3d>] 2nd set of 3D points
         | 
| 2892 | 
            +
              # @param flat [Bool] whether points are to be pre-flattened (Z=0)
         | 
| 1464 2893 | 
             
              #
         | 
| 1465 | 
            -
              # @return [ | 
| 1466 | 
            -
              # @return [ | 
| 1467 | 
            -
              def  | 
| 1468 | 
            -
                mth | 
| 1469 | 
            -
                 | 
| 1470 | 
            -
                 | 
| 2894 | 
            +
              # @return [Bool] whether polygons overlap (or fit)
         | 
| 2895 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 2896 | 
            +
              def overlaps?(p1 = nil, p2 = nil, flat = true)
         | 
| 2897 | 
            +
                mth  = "OSut::#{__callee__}"
         | 
| 2898 | 
            +
                p1   = poly(p1, false, true, false)
         | 
| 2899 | 
            +
                p2   = poly(p2, false, true, false)
         | 
| 2900 | 
            +
                flat = true unless [true, false].include?(flat)
         | 
| 2901 | 
            +
                return false    if p1.empty?
         | 
| 2902 | 
            +
                return false    if p2.empty?
         | 
| 2903 | 
            +
             | 
| 2904 | 
            +
                # Aligned, clockwise & convex points using transformation from 1st polygon.
         | 
| 2905 | 
            +
                t  = OpenStudio::Transformation.alignFace(p1)
         | 
| 2906 | 
            +
                p1 = poly(p1, false, false, true, t, :cw)
         | 
| 2907 | 
            +
                p2 = poly(p2, false, false, true, t, :cw)
         | 
| 2908 | 
            +
                p1 = flatten(p1) if flat
         | 
| 2909 | 
            +
                p2 = flatten(p2) if flat
         | 
| 2910 | 
            +
                return false     if p1.empty?
         | 
| 2911 | 
            +
                return false     if p2.empty?
         | 
| 2912 | 
            +
             | 
| 2913 | 
            +
                return true if fits?(p1, p2)
         | 
| 2914 | 
            +
                return true if fits?(p2, p1)
         | 
| 2915 | 
            +
             | 
| 2916 | 
            +
                area1 = OpenStudio.getArea(p1)
         | 
| 2917 | 
            +
                area2 = OpenStudio.getArea(p2)
         | 
| 2918 | 
            +
                return empty("points 1 area", mth, ERR, false) if area1.empty?
         | 
| 2919 | 
            +
                return empty("points 2 area", mth, ERR, false) if area2.empty?
         | 
| 1471 2920 |  | 
| 1472 | 
            -
                 | 
| 1473 | 
            -
                 | 
| 1474 | 
            -
                 | 
| 2921 | 
            +
                area1 = area1.get
         | 
| 2922 | 
            +
                area2 = area2.get
         | 
| 2923 | 
            +
                union = OpenStudio.join(p1, p2, TOL2)
         | 
| 2924 | 
            +
                return false if union.empty?
         | 
| 1475 2925 |  | 
| 1476 | 
            -
                 | 
| 1477 | 
            -
                 | 
| 1478 | 
            -
                return | 
| 1479 | 
            -
                return  invalid("width", mth, 2, DBG, p1)       unless w.respond_to?(:to_f)
         | 
| 2926 | 
            +
                union = union.get
         | 
| 2927 | 
            +
                area  = OpenStudio.getArea(union)
         | 
| 2928 | 
            +
                return false if area.empty?
         | 
| 1480 2929 |  | 
| 1481 | 
            -
                 | 
| 1482 | 
            -
                 | 
| 2930 | 
            +
                area  = area.get
         | 
| 2931 | 
            +
                delta = area1 + area2 - area
         | 
| 1483 2932 |  | 
| 1484 | 
            -
                 | 
| 1485 | 
            -
             | 
| 1486 | 
            -
             | 
| 2933 | 
            +
                if area > TOL
         | 
| 2934 | 
            +
                  return false if (area - area1).abs < TOL
         | 
| 2935 | 
            +
                  return false if (area - area2).abs < TOL
         | 
| 2936 | 
            +
                  return false if delta.abs < TOL
         | 
| 2937 | 
            +
                  return true  if delta > TOL
         | 
| 2938 | 
            +
                end
         | 
| 1487 2939 |  | 
| 1488 | 
            -
                 | 
| 2940 | 
            +
                false
         | 
| 2941 | 
            +
              end
         | 
| 1489 2942 |  | 
| 1490 | 
            -
             | 
| 1491 | 
            -
             | 
| 1492 | 
            -
             | 
| 1493 | 
            -
             | 
| 1494 | 
            -
             | 
| 2943 | 
            +
              ##
         | 
| 2944 | 
            +
              # Generates offset vertices (by width) for a 3- or 4-sided, convex polygon.
         | 
| 2945 | 
            +
              #
         | 
| 2946 | 
            +
              # @param p1 [Set<OpenStudio::Point3d>] OpenStudio 3D points
         | 
| 2947 | 
            +
              # @param w [#to_f] offset width (min: 0.0254m)
         | 
| 2948 | 
            +
              # @param v [#to_i] OpenStudio SDK version, eg '321' for "v3.2.1" (optional)
         | 
| 2949 | 
            +
              #
         | 
| 2950 | 
            +
              # @return [OpenStudio::Point3dVector] offset points (see logs if unaltered)
         | 
| 2951 | 
            +
              def offset(p1 = nil, w = 0, v = 0)
         | 
| 2952 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 2953 | 
            +
                pts = poly(p1, true, true, false, true, :cw)
         | 
| 2954 | 
            +
                return invalid("points", mth, 1, DBG, p1) unless [3, 4].include?(pts.size)
         | 
| 1495 2955 |  | 
| 1496 | 
            -
             | 
| 1497 | 
            -
             | 
| 1498 | 
            -
                  offset = OpenStudio.buffer(ft_pts, w, TOL)
         | 
| 1499 | 
            -
                  return   p1                                       if offset.empty?
         | 
| 2956 | 
            +
                mismatch("width",   w, Numeric, mth) unless w.respond_to?(:to_f)
         | 
| 2957 | 
            +
                mismatch("version", v, Integer, mth) unless v.respond_to?(:to_i)
         | 
| 1500 2958 |  | 
| 1501 | 
            -
             | 
| 1502 | 
            -
             | 
| 1503 | 
            -
             | 
| 2959 | 
            +
                vs = OpenStudio.openStudioVersion.split(".").join.to_i
         | 
| 2960 | 
            +
                iv = true   if pts.size == 4
         | 
| 2961 | 
            +
                v  = v.to_i if v.respond_to?(:to_i)
         | 
| 2962 | 
            +
                v  = -1 unless v.respond_to?(:to_i)
         | 
| 2963 | 
            +
                v  = vs     if v < 0
         | 
| 2964 | 
            +
                w  = w.to_f if w.respond_to?(:to_f)
         | 
| 2965 | 
            +
                w  = 0  unless w.respond_to?(:to_f)
         | 
| 2966 | 
            +
                w  = 0      if w < 0.0254
         | 
| 1504 2967 |  | 
| 1505 | 
            -
             | 
| 1506 | 
            -
                   | 
| 2968 | 
            +
                unless v < 340
         | 
| 2969 | 
            +
                  t      = OpenStudio::Transformation.alignFace(p1)
         | 
| 2970 | 
            +
                  offset = OpenStudio.buffer(pts, w, TOL)
         | 
| 2971 | 
            +
                  return p1 if offset.empty?
         | 
| 1507 2972 |  | 
| 1508 | 
            -
                  return  | 
| 1509 | 
            -
                else | 
| 2973 | 
            +
                  return to_p3Dv(t * offset.get.reverse)
         | 
| 2974 | 
            +
                else                                                 # brute force approach
         | 
| 1510 2975 | 
             
                  pz     = {}
         | 
| 1511 2976 | 
             
                  pz[:A] = {}
         | 
| 1512 2977 | 
             
                  pz[:B] = {}
         | 
| @@ -1690,81 +3155,347 @@ module OSut | |
| 1690 3155 | 
             
              end
         | 
| 1691 3156 |  | 
| 1692 3157 | 
             
              ##
         | 
| 1693 | 
            -
              #  | 
| 3158 | 
            +
              # Generates a ULC OpenStudio 3D point vector (a bounding box) that surrounds
         | 
| 3159 | 
            +
              # multiple (smaller) OpenStudio 3D point vectors. The generated, 4-point
         | 
| 3160 | 
            +
              # outline is optionally buffered (or offset). Frame and Divider frame widths
         | 
| 3161 | 
            +
              # are taken into account.
         | 
| 3162 | 
            +
              #
         | 
| 3163 | 
            +
              # @param a [Array] sets of OpenStudio 3D points
         | 
| 3164 | 
            +
              # @param bfr [Numeric] an optional buffer size (min: 0.0254m)
         | 
| 3165 | 
            +
              # @param flat [Bool] if points are to be pre-flattened (Z=0)
         | 
| 3166 | 
            +
              #
         | 
| 3167 | 
            +
              # @return [OpenStudio::Point3dVector] ULC outline (see logs if empty)
         | 
| 3168 | 
            +
              def outline(a = [], bfr = 0, flat = true)
         | 
| 3169 | 
            +
                mth  = "OSut::#{__callee__}"
         | 
| 3170 | 
            +
                flat = true unless [true, false].include?(flat)
         | 
| 3171 | 
            +
                xMIN = nil
         | 
| 3172 | 
            +
                xMAX = nil
         | 
| 3173 | 
            +
                yMIN = nil
         | 
| 3174 | 
            +
                yMAX = nil
         | 
| 3175 | 
            +
                a2   = []
         | 
| 3176 | 
            +
                out  = OpenStudio::Point3dVector.new
         | 
| 3177 | 
            +
                cl   = Array
         | 
| 3178 | 
            +
                return mismatch("array", a, cl, mth, DBG, out) unless a.is_a?(cl)
         | 
| 3179 | 
            +
                return empty("array",           mth, DBG, out)     if a.empty?
         | 
| 3180 | 
            +
             | 
| 3181 | 
            +
                mismatch("buffer", bfr, Numeric, mth) unless bfr.respond_to?(:to_f)
         | 
| 3182 | 
            +
             | 
| 3183 | 
            +
                bfr = bfr.to_f if bfr.respond_to?(:to_f)
         | 
| 3184 | 
            +
                bfr = 0    unless bfr.respond_to?(:to_f)
         | 
| 3185 | 
            +
                bfr = 0        if bfr < 0.0254
         | 
| 3186 | 
            +
                vtx = poly(a.first)
         | 
| 3187 | 
            +
                t   = OpenStudio::Transformation.alignFace(vtx) unless vtx.empty?
         | 
| 3188 | 
            +
                return out                                          if vtx.empty?
         | 
| 3189 | 
            +
             | 
| 3190 | 
            +
                a.each do |pts|
         | 
| 3191 | 
            +
                  points = poly(pts, false, true, false, t)
         | 
| 3192 | 
            +
                  points = flatten(points) if flat
         | 
| 3193 | 
            +
                  next if points.empty?
         | 
| 3194 | 
            +
             | 
| 3195 | 
            +
                  a2 << points
         | 
| 3196 | 
            +
                end
         | 
| 3197 | 
            +
             | 
| 3198 | 
            +
                a2.each do |pts|
         | 
| 3199 | 
            +
                  minX = pts.min_by(&:x).x
         | 
| 3200 | 
            +
                  maxX = pts.max_by(&:x).x
         | 
| 3201 | 
            +
                  minY = pts.min_by(&:y).y
         | 
| 3202 | 
            +
                  maxY = pts.max_by(&:y).y
         | 
| 3203 | 
            +
             | 
| 3204 | 
            +
                  # Consider frame width, if frame-and-divider-enabled sub surface.
         | 
| 3205 | 
            +
                  if pts.respond_to?(:allowWindowPropertyFrameAndDivider)
         | 
| 3206 | 
            +
                    fd = pts.windowPropertyFrameAndDivider
         | 
| 3207 | 
            +
                    w  = 0
         | 
| 3208 | 
            +
                    w  = fd.get.frameWidth unless fd.empty?
         | 
| 3209 | 
            +
             | 
| 3210 | 
            +
                    if w > TOL
         | 
| 3211 | 
            +
                      minX -= w
         | 
| 3212 | 
            +
                      maxX += w
         | 
| 3213 | 
            +
                      minY -= w
         | 
| 3214 | 
            +
                      maxY += w
         | 
| 3215 | 
            +
                    end
         | 
| 3216 | 
            +
                  end
         | 
| 3217 | 
            +
             | 
| 3218 | 
            +
                  xMIN = minX if xMIN.nil?
         | 
| 3219 | 
            +
                  xMAX = maxX if xMAX.nil?
         | 
| 3220 | 
            +
                  yMIN = minY if yMIN.nil?
         | 
| 3221 | 
            +
                  yMAX = maxY if yMAX.nil?
         | 
| 3222 | 
            +
             | 
| 3223 | 
            +
                  xMIN = [xMIN, minX].min
         | 
| 3224 | 
            +
                  xMAX = [xMAX, maxX].max
         | 
| 3225 | 
            +
                  yMIN = [yMIN, minY].min
         | 
| 3226 | 
            +
                  yMAX = [yMAX, maxY].max
         | 
| 3227 | 
            +
                end
         | 
| 3228 | 
            +
             | 
| 3229 | 
            +
                return negative("outline width",  mth, DBG, out) if xMAX < xMIN
         | 
| 3230 | 
            +
                return negative("outline height", mth, DBG, out) if yMAX < yMIN
         | 
| 3231 | 
            +
                return zero("outline width",      mth, DBG, out) if (xMIN - xMAX).abs < TOL
         | 
| 3232 | 
            +
                return zero("outline height",     mth, DBG, out) if (yMIN - yMAX).abs < TOL
         | 
| 3233 | 
            +
             | 
| 3234 | 
            +
                # Generate ULC point 3D vector.
         | 
| 3235 | 
            +
                out << OpenStudio::Point3d.new(xMIN, yMAX, 0)
         | 
| 3236 | 
            +
                out << OpenStudio::Point3d.new(xMIN, yMIN, 0)
         | 
| 3237 | 
            +
                out << OpenStudio::Point3d.new(xMAX, yMIN, 0)
         | 
| 3238 | 
            +
                out << OpenStudio::Point3d.new(xMAX, yMAX, 0)
         | 
| 3239 | 
            +
             | 
| 3240 | 
            +
                # Apply buffer, apply ULC (options).
         | 
| 3241 | 
            +
                out = offset(out, bfr, 300) if bfr > 0.0254
         | 
| 3242 | 
            +
             | 
| 3243 | 
            +
                to_p3Dv(t * out)
         | 
| 3244 | 
            +
              end
         | 
| 3245 | 
            +
             | 
| 3246 | 
            +
              ##
         | 
| 3247 | 
            +
              # Returns an array of OpenStudio space-specific surfaces that match criteria,
         | 
| 3248 | 
            +
              # e.g. exterior, north-east facing walls in hotel "lobby". Note 'sides' rely
         | 
| 3249 | 
            +
              # on space coordinates (not absolute model coordinates). And 'sides' are
         | 
| 3250 | 
            +
              # exclusive, not inclusive (e.g. walls strictly north-facing or strictly
         | 
| 3251 | 
            +
              # east-facing would not be returned if 'sides' holds [:north, :east]).
         | 
| 3252 | 
            +
              #
         | 
| 3253 | 
            +
              # @param spaces [Array<OpenStudio::Model::Space>] target spaces
         | 
| 3254 | 
            +
              # @param boundary [#to_s] OpenStudio outside boundary condition
         | 
| 3255 | 
            +
              # @param type [#to_s] OpenStudio surface type
         | 
| 3256 | 
            +
              # @param sides [Array<Symbols>] direction keys, e.g. :north (see OSut::SIDZ)
         | 
| 3257 | 
            +
              #
         | 
| 3258 | 
            +
              # @return [Array<OpenStudio::Model::Surface>] surfaces (may be empty)
         | 
| 3259 | 
            +
              def facets(spaces = [], boundary = "Outdoors", type = "Wall", sides = [])
         | 
| 3260 | 
            +
                return [] unless spaces.respond_to?(:&)
         | 
| 3261 | 
            +
                return [] unless sides.respond_to?(:&)
         | 
| 3262 | 
            +
                return []     if sides.empty?
         | 
| 3263 | 
            +
             | 
| 3264 | 
            +
                faces    = []
         | 
| 3265 | 
            +
                boundary = trim(boundary).downcase
         | 
| 3266 | 
            +
                type     = trim(type).downcase
         | 
| 3267 | 
            +
                return [] if boundary.empty?
         | 
| 3268 | 
            +
                return [] if type.empty?
         | 
| 3269 | 
            +
             | 
| 3270 | 
            +
                # Keep valid sides.
         | 
| 3271 | 
            +
                sides = sides.select { |side| SIDZ.include?(side) }
         | 
| 3272 | 
            +
                return [] if sides.empty?
         | 
| 3273 | 
            +
             | 
| 3274 | 
            +
                spaces.each do |space|
         | 
| 3275 | 
            +
                  return [] unless space.respond_to?(:setSpaceType)
         | 
| 3276 | 
            +
             | 
| 3277 | 
            +
                  space.surfaces.each do |s|
         | 
| 3278 | 
            +
                    next unless s.outsideBoundaryCondition.downcase == boundary
         | 
| 3279 | 
            +
                    next unless s.surfaceType.downcase == type
         | 
| 3280 | 
            +
             | 
| 3281 | 
            +
                    orientations = []
         | 
| 3282 | 
            +
                    orientations << :top    if s.outwardNormal.z >  TOL
         | 
| 3283 | 
            +
                    orientations << :bottom if s.outwardNormal.z < -TOL
         | 
| 3284 | 
            +
                    orientations << :north  if s.outwardNormal.y >  TOL
         | 
| 3285 | 
            +
                    orientations << :east   if s.outwardNormal.x >  TOL
         | 
| 3286 | 
            +
                    orientations << :south  if s.outwardNormal.y < -TOL
         | 
| 3287 | 
            +
                    orientations << :west   if s.outwardNormal.x < -TOL
         | 
| 3288 | 
            +
             | 
| 3289 | 
            +
                    faces << s if sides.all? { |o| orientations.include?(o) }
         | 
| 3290 | 
            +
                  end
         | 
| 3291 | 
            +
                end
         | 
| 3292 | 
            +
             | 
| 3293 | 
            +
                faces
         | 
| 3294 | 
            +
              end
         | 
| 3295 | 
            +
             | 
| 3296 | 
            +
              ##
         | 
| 3297 | 
            +
              # Generates an OpenStudio 3D point vector of a composite floor "slab", a
         | 
| 3298 | 
            +
              # 'union' of multiple rectangular, horizontal floor "plates". Each plate
         | 
| 3299 | 
            +
              # must either share an edge with (or encompass or overlap) any of the
         | 
| 3300 | 
            +
              # preceding plates in the array. The generated slab may not be convex.
         | 
| 3301 | 
            +
              #
         | 
| 3302 | 
            +
              # @param [Array<Hash>] pltz individual floor plates, each holding:
         | 
| 3303 | 
            +
              # @option pltz [Numeric] :x left corner of plate origin (bird's eye view)
         | 
| 3304 | 
            +
              # @option pltz [Numeric] :y bottom corner of plate origin (bird's eye view)
         | 
| 3305 | 
            +
              # @option pltz [Numeric] :dx plate width (bird's eye view)
         | 
| 3306 | 
            +
              # @option pltz [Numeric] :dy plate depth (bird's eye view)
         | 
| 3307 | 
            +
              # @param z [Numeric] Z-axis coordinate
         | 
| 3308 | 
            +
              #
         | 
| 3309 | 
            +
              # @return [OpenStudio::Point3dVector] slab vertices (see logs if empty)
         | 
| 3310 | 
            +
              def genSlab(pltz = [], z = 0)
         | 
| 3311 | 
            +
                mth = "OSut::#{__callee__}"
         | 
| 3312 | 
            +
                slb = OpenStudio::Point3dVector.new
         | 
| 3313 | 
            +
                bkp = OpenStudio::Point3dVector.new
         | 
| 3314 | 
            +
                cl1 = Array
         | 
| 3315 | 
            +
                cl2 = Hash
         | 
| 3316 | 
            +
                cl3 = Numeric
         | 
| 3317 | 
            +
             | 
| 3318 | 
            +
                # Input validation.
         | 
| 3319 | 
            +
                return mismatch("plates", pltz, cl1, mth, DBG, slb) unless pltz.is_a?(cl1)
         | 
| 3320 | 
            +
                return mismatch(     "Z",    z, cl3, mth, DBG, slb) unless z.is_a?(cl3)
         | 
| 3321 | 
            +
             | 
| 3322 | 
            +
                pltz.each_with_index do |plt, i|
         | 
| 3323 | 
            +
                  id = "plate # #{i+1} (index #{i})"
         | 
| 3324 | 
            +
             | 
| 3325 | 
            +
                  return mismatch(id, plt, cl1, mth, DBG, slb) unless plt.is_a?(cl2)
         | 
| 3326 | 
            +
                  return hashkey( id, plt,  :x, mth, DBG, slb) unless plt.key?(:x )
         | 
| 3327 | 
            +
                  return hashkey( id, plt,  :y, mth, DBG, slb) unless plt.key?(:y )
         | 
| 3328 | 
            +
                  return hashkey( id, plt, :dx, mth, DBG, slb) unless plt.key?(:dx)
         | 
| 3329 | 
            +
                  return hashkey( id, plt, :dy, mth, DBG, slb) unless plt.key?(:dy)
         | 
| 3330 | 
            +
             | 
| 3331 | 
            +
                  x  = plt[:x ]
         | 
| 3332 | 
            +
                  y  = plt[:y ]
         | 
| 3333 | 
            +
                  dx = plt[:dx]
         | 
| 3334 | 
            +
                  dy = plt[:dy]
         | 
| 3335 | 
            +
             | 
| 3336 | 
            +
                  return mismatch("#{id} X",   x, cl3, mth, DBG, slb) unless  x.is_a?(cl3)
         | 
| 3337 | 
            +
                  return mismatch("#{id} Y",   y, cl3, mth, DBG, slb) unless  y.is_a?(cl3)
         | 
| 3338 | 
            +
                  return mismatch("#{id} dX", dx, cl3, mth, DBG, slb) unless dx.is_a?(cl3)
         | 
| 3339 | 
            +
                  return mismatch("#{id} dY", dy, cl3, mth, DBG, slb) unless dy.is_a?(cl3)
         | 
| 3340 | 
            +
                  return zero(    "#{id} dX",          mth, ERR, slb)     if dx.abs < TOL
         | 
| 3341 | 
            +
                  return zero(    "#{id} dY",          mth, ERR, slb)     if dy.abs < TOL
         | 
| 3342 | 
            +
                end
         | 
| 3343 | 
            +
             | 
| 3344 | 
            +
                # Join plates.
         | 
| 3345 | 
            +
                pltz.each_with_index do |plt, i|
         | 
| 3346 | 
            +
                  id = "plate # #{i+1} (index #{i})"
         | 
| 3347 | 
            +
                  x  = plt[:x ]
         | 
| 3348 | 
            +
                  y  = plt[:y ]
         | 
| 3349 | 
            +
                  dx = plt[:dx]
         | 
| 3350 | 
            +
                  dy = plt[:dy]
         | 
| 3351 | 
            +
             | 
| 3352 | 
            +
                  # Adjust X if dX < 0.
         | 
| 3353 | 
            +
                  x -= -dx if dx < 0
         | 
| 3354 | 
            +
                  dx = -dx if dx < 0
         | 
| 3355 | 
            +
             | 
| 3356 | 
            +
                  # Adjust Y if dY < 0.
         | 
| 3357 | 
            +
                  y -= -dy if dy < 0
         | 
| 3358 | 
            +
                  dy = -dy if dy < 0
         | 
| 3359 | 
            +
             | 
| 3360 | 
            +
                  vtx  = []
         | 
| 3361 | 
            +
                  vtx << OpenStudio::Point3d.new(x + dx, y + dy, 0)
         | 
| 3362 | 
            +
                  vtx << OpenStudio::Point3d.new(x + dx, y,      0)
         | 
| 3363 | 
            +
                  vtx << OpenStudio::Point3d.new(x,      y,      0)
         | 
| 3364 | 
            +
                  vtx << OpenStudio::Point3d.new(x,      y + dy, 0)
         | 
| 3365 | 
            +
             | 
| 3366 | 
            +
                  if slb.empty?
         | 
| 3367 | 
            +
                    slb = vtx
         | 
| 3368 | 
            +
                  else
         | 
| 3369 | 
            +
                    slab = OpenStudio.join(slb, vtx, TOL2)
         | 
| 3370 | 
            +
                    slb  = slab.get                  unless slab.empty?
         | 
| 3371 | 
            +
                    return invalid(id, mth, 0, ERR, bkp) if slab.empty?
         | 
| 3372 | 
            +
                  end
         | 
| 3373 | 
            +
                end
         | 
| 3374 | 
            +
             | 
| 3375 | 
            +
                # Once joined, re-adjust Z-axis coordinates.
         | 
| 3376 | 
            +
                unless z.zero?
         | 
| 3377 | 
            +
                  vtx = OpenStudio::Point3dVector.new
         | 
| 3378 | 
            +
                  slb.each { |pt| vtx << OpenStudio::Point3d.new(pt.x, pt.y, z) }
         | 
| 3379 | 
            +
                  slb = vtx
         | 
| 3380 | 
            +
                end
         | 
| 3381 | 
            +
             | 
| 3382 | 
            +
                slb
         | 
| 3383 | 
            +
              end
         | 
| 3384 | 
            +
             | 
| 3385 | 
            +
              ##
         | 
| 3386 | 
            +
              # Returns outdoor-facing, space-(related) roof/ceiling surfaces. These
         | 
| 3387 | 
            +
              # include outdoor-facing roof/ceilings of the space per se, as well as
         | 
| 3388 | 
            +
              # any outside-facing roof/ceiling surface of an unoccupied space
         | 
| 3389 | 
            +
              # immediately above (e.g. a plenum) overlapping any of the roof/ceilings
         | 
| 3390 | 
            +
              # of the space itself.
         | 
| 1694 3391 | 
             
              #
         | 
| 1695 | 
            -
              # @param  | 
| 3392 | 
            +
              # @param space [OpenStudio::Model::Space] a space
         | 
| 1696 3393 | 
             
              #
         | 
| 1697 | 
            -
              # @return [ | 
| 1698 | 
            -
              def  | 
| 3394 | 
            +
              # @return [Array<OpenStudio::Model::Surface>] surfaces (see logs if empty)
         | 
| 3395 | 
            +
              def getRoofs(space = nil)
         | 
| 1699 3396 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1700 | 
            -
                cl | 
| 3397 | 
            +
                cl  = OpenStudio::Model::Space
         | 
| 3398 | 
            +
                return mismatch("space", space, cl, mth, DBG, []) unless space.is_a?(cl)
         | 
| 3399 | 
            +
             | 
| 3400 | 
            +
                roofs = space.surfaces # outdoor-facing roofs of the space
         | 
| 3401 | 
            +
                clngs = space.surfaces # surface-facing ceilings of the space
         | 
| 3402 | 
            +
             | 
| 3403 | 
            +
                roofs = roofs.select {|s| s.surfaceType.downcase == "roofceiling"}
         | 
| 3404 | 
            +
                roofs = roofs.select {|s| s.outsideBoundaryCondition.downcase == "outdoors"}
         | 
| 1701 3405 |  | 
| 1702 | 
            -
                 | 
| 3406 | 
            +
                clngs = clngs.select {|s| s.surfaceType.downcase == "roofceiling"}
         | 
| 3407 | 
            +
                clngs = clngs.select {|s| s.outsideBoundaryCondition.downcase == "surface"}
         | 
| 1703 3408 |  | 
| 1704 | 
            -
                 | 
| 1705 | 
            -
             | 
| 1706 | 
            -
             | 
| 3409 | 
            +
                clngs.each do |ceiling|
         | 
| 3410 | 
            +
                  floor = ceiling.adjacentSurface
         | 
| 3411 | 
            +
                  next if floor.empty?
         | 
| 1707 3412 |  | 
| 1708 | 
            -
             | 
| 1709 | 
            -
             | 
| 3413 | 
            +
                  other = floor.get.space
         | 
| 3414 | 
            +
                  next if other.empty?
         | 
| 1710 3415 |  | 
| 1711 | 
            -
             | 
| 1712 | 
            -
                  v1  = s.vertices[i]
         | 
| 1713 | 
            -
                  v2  = s.vertices[i + 1]                                  unless i == last
         | 
| 1714 | 
            -
                  v2  = s.vertices.first                                       if i == last
         | 
| 1715 | 
            -
                  vec = v2 - v1
         | 
| 1716 | 
            -
                  bad = vec.length < TOL
         | 
| 3416 | 
            +
                  rufs = other.get.surfaces
         | 
| 1717 3417 |  | 
| 1718 | 
            -
                   | 
| 1719 | 
            -
                   | 
| 1720 | 
            -
                   | 
| 1721 | 
            -
             | 
| 3418 | 
            +
                  rufs = rufs.select {|s| s.surfaceType.downcase == "roofceiling"}
         | 
| 3419 | 
            +
                  rufs = rufs.select {|s| s.outsideBoundaryCondition.downcase == "outdoors"}
         | 
| 3420 | 
            +
                  next if rufs.empty?
         | 
| 3421 | 
            +
             | 
| 3422 | 
            +
                  # Only keep track of "other" roof(s) that "overlap" ceiling below.
         | 
| 3423 | 
            +
                  rufs.each do |ruf|
         | 
| 3424 | 
            +
                    next unless overlaps?(ceiling, ruf)
         | 
| 3425 | 
            +
             | 
| 3426 | 
            +
                    roofs << ruf unless roofs.include?(ruf)
         | 
| 3427 | 
            +
                  end
         | 
| 1722 3428 | 
             
                end
         | 
| 1723 3429 |  | 
| 1724 | 
            -
                 | 
| 1725 | 
            -
                true
         | 
| 3430 | 
            +
                roofs
         | 
| 1726 3431 | 
             
              end
         | 
| 1727 3432 |  | 
| 1728 3433 | 
             
              ##
         | 
| 1729 | 
            -
              #  | 
| 3434 | 
            +
              # Adds sub surfaces (e.g. windows, doors, skylights) to surface.
         | 
| 1730 3435 | 
             
              #
         | 
| 1731 | 
            -
              # @param model [OpenStudio::Model::Model] a model
         | 
| 1732 3436 | 
             
              # @param s [OpenStudio::Model::Surface] a model surface
         | 
| 1733 | 
            -
              # @param  | 
| 1734 | 
            -
              # @ | 
| 1735 | 
            -
              # @ | 
| 1736 | 
            -
              #
         | 
| 1737 | 
            -
              # @ | 
| 1738 | 
            -
               | 
| 3437 | 
            +
              # @param [Array<Hash>] subs requested attributes
         | 
| 3438 | 
            +
              # @option subs [#to_s] :id identifier e.g. "Window 007"
         | 
| 3439 | 
            +
              # @option subs [#to_s] :type ("FixedWindow") OpenStudio subsurface type
         | 
| 3440 | 
            +
              # @option subs [#to_i] :count (1) number of individual subs per array
         | 
| 3441 | 
            +
              # @option subs [#to_i] :multiplier (1) OpenStudio subsurface multiplier
         | 
| 3442 | 
            +
              # @option subs [#frameWidth] :frame (nil) OpenStudio frame & divider object
         | 
| 3443 | 
            +
              # @option subs [#isFenestration] :assembly (nil) OpenStudio construction
         | 
| 3444 | 
            +
              # @option subs [#to_f] :ratio e.g. %FWR [0.0, 1.0]
         | 
| 3445 | 
            +
              # @option subs [#to_f] :head (OSut::HEAD) e.g. door height (incl frame)
         | 
| 3446 | 
            +
              # @option subs [#to_f] :sill (OSut::SILL) e.g. window sill (incl frame)
         | 
| 3447 | 
            +
              # @option subs [#to_f] :height sill-to-head height
         | 
| 3448 | 
            +
              # @option subs [#to_f] :width e.g. door width
         | 
| 3449 | 
            +
              # @option subs [#to_f] :offset left-right centreline dX e.g. between doors
         | 
| 3450 | 
            +
              # @option subs [#to_f] :centreline left-right dX (sub/array vs base)
         | 
| 3451 | 
            +
              # @option subs [#to_f] :r_buffer gap between sub/array and right corner
         | 
| 3452 | 
            +
              # @option subs [#to_f] :l_buffer gap between sub/array and left corner
         | 
| 3453 | 
            +
              # @param clear [Bool] whether to remove current sub surfaces
         | 
| 3454 | 
            +
              # @param bfr [#to_f] safety buffer, to maintain near other edges
         | 
| 3455 | 
            +
              #
         | 
| 3456 | 
            +
              # @return [Bool] whether addition is successful
         | 
| 3457 | 
            +
              # @return [false] if invalid input (see logs)
         | 
| 3458 | 
            +
              def addSubs(s = nil, subs = [], clear = false, bfr = 0.005)
         | 
| 1739 3459 | 
             
                mth = "OSut::#{__callee__}"
         | 
| 1740 3460 | 
             
                v   = OpenStudio.openStudioVersion.split(".").join.to_i
         | 
| 1741 | 
            -
                cl1 = OpenStudio::Model:: | 
| 1742 | 
            -
                cl2 =  | 
| 1743 | 
            -
                cl3 =  | 
| 1744 | 
            -
                cl4 = Hash
         | 
| 1745 | 
            -
                cl5 = Numeric
         | 
| 3461 | 
            +
                cl1 = OpenStudio::Model::Surface
         | 
| 3462 | 
            +
                cl2 = Array
         | 
| 3463 | 
            +
                cl3 = Hash
         | 
| 1746 3464 | 
             
                min = 0.050 # minimum ratio value ( 5%)
         | 
| 1747 3465 | 
             
                max = 0.950 # maximum ratio value (95%)
         | 
| 1748 3466 | 
             
                no  = false
         | 
| 1749 3467 |  | 
| 1750 3468 | 
             
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 1751 3469 | 
             
                # Exit if mismatched or invalid argument classes.
         | 
| 1752 | 
            -
                return mismatch(" | 
| 1753 | 
            -
                return mismatch(" | 
| 1754 | 
            -
                return  | 
| 1755 | 
            -
                return no                                          unless surface_valid?(s)
         | 
| 3470 | 
            +
                return mismatch("surface",  s, cl2, mth, DBG, no) unless s.is_a?(cl1)
         | 
| 3471 | 
            +
                return mismatch("subs",  subs, cl3, mth, DBG, no) unless subs.is_a?(cl2)
         | 
| 3472 | 
            +
                return empty("surface points",      mth, DBG, no)     if poly(s).empty?
         | 
| 1756 3473 |  | 
| 1757 3474 | 
             
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 1758 3475 | 
             
                # Clear existing sub surfaces if requested.
         | 
| 1759 3476 | 
             
                nom = s.nameString
         | 
| 3477 | 
            +
                mdl = s.model
         | 
| 1760 3478 |  | 
| 1761 | 
            -
                unless  | 
| 3479 | 
            +
                unless [true, false].include?(clear)
         | 
| 1762 3480 | 
             
                  log(WRN, "#{nom}: Keeping existing sub surfaces (#{mth})")
         | 
| 1763 3481 | 
             
                  clear = false
         | 
| 1764 3482 | 
             
                end
         | 
| 1765 3483 |  | 
| 1766 3484 | 
             
                s.subSurfaces.map(&:remove) if clear
         | 
| 1767 3485 |  | 
| 3486 | 
            +
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 3487 | 
            +
                # Ensure minimum safety buffer.
         | 
| 3488 | 
            +
                if bfr.respond_to?(:to_f)
         | 
| 3489 | 
            +
                  bfr = bfr.to_f
         | 
| 3490 | 
            +
                  return negative("safety buffer", mth, ERR, no) if bfr < 0
         | 
| 3491 | 
            +
             | 
| 3492 | 
            +
                  msg = "Safety buffer < 5mm may generate invalid geometry (#{mth})"
         | 
| 3493 | 
            +
                  log(WRN, msg) if bfr < 0.005
         | 
| 3494 | 
            +
                else
         | 
| 3495 | 
            +
                  log(ERR, "Setting safety buffer to 5mm (#{mth})")
         | 
| 3496 | 
            +
                  bfr = 0.005
         | 
| 3497 | 
            +
                end
         | 
| 3498 | 
            +
             | 
| 1768 3499 | 
             
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 1769 3500 | 
             
                # Allowable sub surface types ... & Frame&Divider enabled
         | 
| 1770 3501 | 
             
                #   - "FixedWindow"             | true
         | 
| @@ -1780,47 +3511,36 @@ module OSut | |
| 1780 3511 | 
             
                stype = s.surfaceType # Wall, RoofCeiling or Floor
         | 
| 1781 3512 |  | 
| 1782 3513 | 
             
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 1783 | 
            -
                 | 
| 1784 | 
            -
                 | 
| 1785 | 
            -
                 | 
| 1786 | 
            -
                 | 
| 1787 | 
            -
                 | 
| 1788 | 
            -
                #   - Z-axis values are represented as Y-axis values
         | 
| 1789 | 
            -
                tr = OpenStudio::Transformation.alignFace(s.vertices)
         | 
| 1790 | 
            -
             | 
| 1791 | 
            -
                # Aligned vertices of host surface, and fetch attributes.
         | 
| 1792 | 
            -
                aligned = tr.inverse * s.vertices
         | 
| 1793 | 
            -
                max_x   = aligned.max_by(&:x).x
         | 
| 1794 | 
            -
                max_y   = aligned.max_by(&:y).y
         | 
| 1795 | 
            -
                mid_x   = max_x / 2
         | 
| 1796 | 
            -
                mid_y   = max_y / 2
         | 
| 3514 | 
            +
                t     = OpenStudio::Transformation.alignFace(s.vertices)
         | 
| 3515 | 
            +
                max_x = width(s)
         | 
| 3516 | 
            +
                max_y = height(s)
         | 
| 3517 | 
            +
                mid_x = max_x / 2
         | 
| 3518 | 
            +
                mid_y = max_y / 2
         | 
| 1797 3519 |  | 
| 1798 3520 | 
             
                # --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- #
         | 
| 1799 3521 | 
             
                # Assign default values to certain sub keys (if missing), +more validation.
         | 
| 1800 3522 | 
             
                subs.each_with_index do |sub, index|
         | 
| 1801 | 
            -
                  return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?( | 
| 3523 | 
            +
                  return mismatch("sub", sub, cl4, mth, DBG, no) unless sub.is_a?(cl3)
         | 
| 1802 3524 |  | 
| 1803 3525 | 
             
                  # Required key:value pairs (either set by the user or defaulted).
         | 
| 1804 | 
            -
                  sub[: | 
| 1805 | 
            -
                  sub[: | 
| 1806 | 
            -
                  sub[:count     ] = 1 | 
| 1807 | 
            -
                  sub[:multiplier] = 1 | 
| 1808 | 
            -
                  sub[: | 
| 1809 | 
            -
                  sub[: | 
| 1810 | 
            -
             | 
| 1811 | 
            -
                   | 
| 1812 | 
            -
                   | 
| 1813 | 
            -
                   | 
| 1814 | 
            -
                   | 
| 1815 | 
            -
                   | 
| 1816 | 
            -
                   | 
| 1817 | 
            -
                   | 
| 1818 | 
            -
                   | 
| 1819 | 
            -
                   | 
| 1820 | 
            -
             | 
| 1821 | 
            -
             | 
| 1822 | 
            -
                  sub[:id] = "#{nom}|#{index}" if sub[:id].empty?
         | 
| 1823 | 
            -
                  id       = sub[:id]
         | 
| 3526 | 
            +
                  sub[:frame     ] = nil  unless sub.key?(:frame     )
         | 
| 3527 | 
            +
                  sub[:assembly  ] = nil  unless sub.key?(:assembly  )
         | 
| 3528 | 
            +
                  sub[:count     ] = 1    unless sub.key?(:count     )
         | 
| 3529 | 
            +
                  sub[:multiplier] = 1    unless sub.key?(:multiplier)
         | 
| 3530 | 
            +
                  sub[:id        ] = ""   unless sub.key?(:id        )
         | 
| 3531 | 
            +
                  sub[:type      ] = type unless sub.key?(:type      )
         | 
| 3532 | 
            +
                  sub[:type      ] = trim(sub[:type])
         | 
| 3533 | 
            +
                  sub[:id        ] = trim(sub[:id])
         | 
| 3534 | 
            +
                  sub[:type      ] = type                   if sub[:type].empty?
         | 
| 3535 | 
            +
                  sub[:id        ] = "OSut|#{nom}|#{index}" if sub[:id  ].empty?
         | 
| 3536 | 
            +
                  sub[:count     ] = 1 unless sub[:count     ].respond_to?(:to_i)
         | 
| 3537 | 
            +
                  sub[:multiplier] = 1 unless sub[:multiplier].respond_to?(:to_i)
         | 
| 3538 | 
            +
                  sub[:count     ] = sub[:count     ].to_i
         | 
| 3539 | 
            +
                  sub[:multiplier] = sub[:multiplier].to_i
         | 
| 3540 | 
            +
                  sub[:count     ] = 1 if sub[:count     ] < 1
         | 
| 3541 | 
            +
                  sub[:multiplier] = 1 if sub[:multiplier] < 1
         | 
| 3542 | 
            +
             | 
| 3543 | 
            +
                  id = sub[:id]
         | 
| 1824 3544 |  | 
| 1825 3545 | 
             
                  # If sub surface type is invalid, log/reset. Additional corrections may
         | 
| 1826 3546 | 
             
                  # be enabled once a sub surface is actually instantiated.
         | 
| @@ -1855,14 +3575,17 @@ module OSut | |
| 1855 3575 | 
             
                    end
         | 
| 1856 3576 | 
             
                  end
         | 
| 1857 3577 |  | 
| 1858 | 
            -
                  # Log/reset negative  | 
| 3578 | 
            +
                  # Log/reset negative float values. Set ~0.0 values to 0.0.
         | 
| 1859 3579 | 
             
                  sub.each do |key, value|
         | 
| 1860 | 
            -
                    next if key == : | 
| 3580 | 
            +
                    next if key == :count
         | 
| 3581 | 
            +
                    next if key == :multiplier
         | 
| 1861 3582 | 
             
                    next if key == :type
         | 
| 3583 | 
            +
                    next if key == :id
         | 
| 1862 3584 | 
             
                    next if key == :frame
         | 
| 1863 3585 | 
             
                    next if key == :assembly
         | 
| 1864 3586 |  | 
| 1865 | 
            -
                     | 
| 3587 | 
            +
                    ok = value.respond_to?(:to_f)
         | 
| 3588 | 
            +
                    return mismatch(key, value, Float, mth, DBG, no) unless ok
         | 
| 1866 3589 | 
             
                    next if key == :centreline
         | 
| 1867 3590 |  | 
| 1868 3591 | 
             
                    negative(key, mth, WRN) if value < 0
         | 
| @@ -1907,9 +3630,9 @@ module OSut | |
| 1907 3630 | 
             
                  max_height = max_y - buffers
         | 
| 1908 3631 | 
             
                  max_width  = max_x - buffers
         | 
| 1909 3632 |  | 
| 1910 | 
            -
                  # Default sub surface "head" & "sill" height  | 
| 1911 | 
            -
                  typ_head = HEAD | 
| 1912 | 
            -
                  typ_sill = SILL | 
| 3633 | 
            +
                  # Default sub surface "head" & "sill" height, unless user-specified.
         | 
| 3634 | 
            +
                  typ_head = HEAD
         | 
| 3635 | 
            +
                  typ_sill = SILL
         | 
| 1913 3636 |  | 
| 1914 3637 | 
             
                  if sub.key?(:ratio)
         | 
| 1915 3638 | 
             
                    typ_head = mid_y * (1 + sub[:ratio])     if sub[:ratio] > 0.75
         | 
| @@ -2051,18 +3774,22 @@ module OSut | |
| 2051 3774 | 
             
                  # Log/reset "width" if beyond min/max.
         | 
| 2052 3775 | 
             
                  if sub.key?(:width)
         | 
| 2053 3776 | 
             
                    unless sub[:width].between?(glass, max_width)
         | 
| 2054 | 
            -
                      sub[:width] = glass | 
| 2055 | 
            -
                      sub[:width] = max_width | 
| 3777 | 
            +
                      sub[:width] = glass     if sub[:width] < glass
         | 
| 3778 | 
            +
                      sub[:width] = max_width if sub[:width] > max_width
         | 
| 2056 3779 | 
             
                      log(WRN, "Reset '#{id}' width to #{sub[:width]} m (#{mth})")
         | 
| 2057 3780 | 
             
                    end
         | 
| 2058 3781 | 
             
                  end
         | 
| 2059 3782 |  | 
| 2060 | 
            -
                  # Log/reset "count" if < 1 | 
| 2061 | 
            -
                  if sub. | 
| 3783 | 
            +
                  # Log/reset "count" if < 1 (or not an Integer)
         | 
| 3784 | 
            +
                  if sub[:count].respond_to?(:to_i)
         | 
| 3785 | 
            +
                    sub[:count] = sub[:count].to_i
         | 
| 3786 | 
            +
             | 
| 2062 3787 | 
             
                    if sub[:count] < 1
         | 
| 2063 3788 | 
             
                      sub[:count] = 1
         | 
| 2064 3789 | 
             
                      log(WRN, "Reset '#{id}' count to #{sub[:count]} (#{mth})")
         | 
| 2065 3790 | 
             
                    end
         | 
| 3791 | 
            +
                  else
         | 
| 3792 | 
            +
                    sub[:count] = 1
         | 
| 2066 3793 | 
             
                  end
         | 
| 2067 3794 |  | 
| 2068 3795 | 
             
                  sub[:count] = 1 unless sub.key?(:count)
         | 
| @@ -2221,7 +3948,7 @@ module OSut | |
| 2221 3948 |  | 
| 2222 3949 | 
             
                  # Generate sub(s).
         | 
| 2223 3950 | 
             
                  sub[:count].times do |i|
         | 
| 2224 | 
            -
                    name = "#{id} | 
| 3951 | 
            +
                    name = "#{id}|#{i}"
         | 
| 2225 3952 | 
             
                    fr   = 0
         | 
| 2226 3953 | 
             
                    fr   = sub[:frame].frameWidth if sub[:frame]
         | 
| 2227 3954 |  | 
| @@ -2230,12 +3957,12 @@ module OSut | |
| 2230 3957 | 
             
                    vec << OpenStudio::Point3d.new(pos,               sub[:sill], 0)
         | 
| 2231 3958 | 
             
                    vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:sill], 0)
         | 
| 2232 3959 | 
             
                    vec << OpenStudio::Point3d.new(pos + sub[:width], sub[:head], 0)
         | 
| 2233 | 
            -
                    vec =  | 
| 3960 | 
            +
                    vec = t * vec
         | 
| 2234 3961 |  | 
| 2235 3962 | 
             
                    # Log/skip if conflict between individual sub and base surface.
         | 
| 2236 3963 | 
             
                    vc = vec
         | 
| 2237 3964 | 
             
                    vc = offset(vc, fr, 300) if fr > 0
         | 
| 2238 | 
            -
                    ok = fits?(vc, s | 
| 3965 | 
            +
                    ok = fits?(vc, s)
         | 
| 2239 3966 | 
             
                    log(ERR, "Skip '#{name}': won't fit in '#{nom}' (#{mth})") unless ok
         | 
| 2240 3967 | 
             
                    break                                                      unless ok
         | 
| 2241 3968 |  | 
| @@ -2247,7 +3974,7 @@ module OSut | |
| 2247 3974 | 
             
                      fr   = fd.get.frameWidth unless fd.empty?
         | 
| 2248 3975 | 
             
                      vk   = sb.vertices
         | 
| 2249 3976 | 
             
                      vk   = offset(vk, fr, 300) if fr > 0
         | 
| 2250 | 
            -
                      oops = overlaps?(vc, vk | 
| 3977 | 
            +
                      oops = overlaps?(vc, vk)
         | 
| 2251 3978 | 
             
                      log(ERR, "Skip '#{name}': overlaps '#{nome}' (#{mth})") if oops
         | 
| 2252 3979 | 
             
                      ok = false                                              if oops
         | 
| 2253 3980 | 
             
                      break                                                   if oops
         | 
| @@ -2255,7 +3982,7 @@ module OSut | |
| 2255 3982 |  | 
| 2256 3983 | 
             
                    break unless ok
         | 
| 2257 3984 |  | 
| 2258 | 
            -
                    sb = OpenStudio::Model::SubSurface.new(vec,  | 
| 3985 | 
            +
                    sb = OpenStudio::Model::SubSurface.new(vec, mdl)
         | 
| 2259 3986 | 
             
                    sb.setName(name)
         | 
| 2260 3987 | 
             
                    sb.setSubSurfaceType(sub[:type])
         | 
| 2261 3988 | 
             
                    sb.setConstruction(sub[:assembly])               if sub[:assembly]
         |