osut 0.2.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 +7 -0
 - data/.gitattributes +3 -0
 - data/.github/workflows/pull_request.yml +72 -0
 - data/.gitignore +11 -0
 - data/.rspec +3 -0
 - data/Gemfile +3 -0
 - data/LICENSE +29 -0
 - data/README.md +54 -0
 - data/Rakefile +11 -0
 - data/lib/osut/utils.rb +1430 -0
 - data/lib/osut/version.rb +33 -0
 - data/lib/osut.rb +39 -0
 - data/osut.gemspec +36 -0
 - metadata +131 -0
 
    
        data/lib/osut/utils.rb
    ADDED
    
    | 
         @@ -0,0 +1,1430 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # BSD 3-Clause License
         
     | 
| 
      
 2 
     | 
    
         
            +
            #
         
     | 
| 
      
 3 
     | 
    
         
            +
            # Copyright (c) 2022, Denis Bourgeois
         
     | 
| 
      
 4 
     | 
    
         
            +
            # All rights reserved.
         
     | 
| 
      
 5 
     | 
    
         
            +
            #
         
     | 
| 
      
 6 
     | 
    
         
            +
            # Redistribution and use in source and binary forms, with or without
         
     | 
| 
      
 7 
     | 
    
         
            +
            # modification, are permitted provided that the following conditions are met:
         
     | 
| 
      
 8 
     | 
    
         
            +
            #
         
     | 
| 
      
 9 
     | 
    
         
            +
            # 1. Redistributions of source code must retain the above copyright notice, this
         
     | 
| 
      
 10 
     | 
    
         
            +
            #    list of conditions and the following disclaimer.
         
     | 
| 
      
 11 
     | 
    
         
            +
            #
         
     | 
| 
      
 12 
     | 
    
         
            +
            # 2. Redistributions in binary form must reproduce the above copyright notice,
         
     | 
| 
      
 13 
     | 
    
         
            +
            #    this list of conditions and the following disclaimer in the documentation
         
     | 
| 
      
 14 
     | 
    
         
            +
            #    and/or other materials provided with the distribution.
         
     | 
| 
      
 15 
     | 
    
         
            +
            #
         
     | 
| 
      
 16 
     | 
    
         
            +
            # 3. Neither the name of the copyright holder nor the names of its
         
     | 
| 
      
 17 
     | 
    
         
            +
            #    contributors may be used to endorse or promote products derived from
         
     | 
| 
      
 18 
     | 
    
         
            +
            #    this software without specific prior written permission.
         
     | 
| 
      
 19 
     | 
    
         
            +
            #
         
     | 
| 
      
 20 
     | 
    
         
            +
            # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
         
     | 
| 
      
 21 
     | 
    
         
            +
            # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
         
     | 
| 
      
 22 
     | 
    
         
            +
            # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
         
     | 
| 
      
 23 
     | 
    
         
            +
            # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
         
     | 
| 
      
 24 
     | 
    
         
            +
            # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
         
     | 
| 
      
 25 
     | 
    
         
            +
            # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
         
     | 
| 
      
 26 
     | 
    
         
            +
            # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
         
     | 
| 
      
 27 
     | 
    
         
            +
            # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
         
     | 
| 
      
 28 
     | 
    
         
            +
            # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
         
     | 
| 
      
 29 
     | 
    
         
            +
            # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
            require "openstudio"
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            module OSut
         
     | 
| 
      
 34 
     | 
    
         
            +
              extend OSlg            #   DEBUG for devs; WARN/ERROR for users (bad OS input)
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              TOL  = 0.01
         
     | 
| 
      
 37 
     | 
    
         
            +
              TOL2 = TOL * TOL
         
     | 
| 
      
 38 
     | 
    
         
            +
              NS   = "nameString"    #                OpenStudio IdfObject nameString method
         
     | 
| 
      
 39 
     | 
    
         
            +
              DBG  = OSut::DEBUG     # mainly to flag invalid arguments to devs (buggy code)
         
     | 
| 
      
 40 
     | 
    
         
            +
              INF  = OSut::INFO      #                            not currently used in OSut
         
     | 
| 
      
 41 
     | 
    
         
            +
              WRN  = OSut::WARN      #   WARN users of 'iffy' .osm inputs (yet not critical)
         
     | 
| 
      
 42 
     | 
    
         
            +
              ERR  = OSut::ERROR     #     flag invalid .osm inputs (then exit via 'return')
         
     | 
| 
      
 43 
     | 
    
         
            +
              FTL  = OSut::FATAL     #                            not currently used in OSut
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
              # This first set of utilities (~750 lines) help distinguishing spaces that
         
     | 
| 
      
 46 
     | 
    
         
            +
              # are directly vs indirectly CONDITIONED, vs SEMI-HEATED. The solution here
         
     | 
| 
      
 47 
     | 
    
         
            +
              # relies as much as possible on space conditioning categories found in
         
     | 
| 
      
 48 
     | 
    
         
            +
              # standards like ASHRAE 90.1 and energy codes like the Canadian NECB editions.
         
     | 
| 
      
 49 
     | 
    
         
            +
              # Both documents share many similarities, regardless of nomenclature. There
         
     | 
| 
      
 50 
     | 
    
         
            +
              # are however noticeable differences between approaches on how a space is
         
     | 
| 
      
 51 
     | 
    
         
            +
              # tagged as falling into one of the aforementioned categories. First, an
         
     | 
| 
      
 52 
     | 
    
         
            +
              # overview of 90.1 requirements, with some minor edits for brevity/emphasis:
         
     | 
| 
      
 53 
     | 
    
         
            +
              #
         
     | 
| 
      
 54 
     | 
    
         
            +
              # www.pnnl.gov/main/publications/external/technical_reports/PNNL-26917.pdf
         
     | 
| 
      
 55 
     | 
    
         
            +
              #
         
     | 
| 
      
 56 
     | 
    
         
            +
              #   3.2.1. General Information - SPACE CONDITIONING CATEGORY
         
     | 
| 
      
 57 
     | 
    
         
            +
              #
         
     | 
| 
      
 58 
     | 
    
         
            +
              #     - CONDITIONED space: an ENCLOSED space that has a heating and/or
         
     | 
| 
      
 59 
     | 
    
         
            +
              #       cooling system of sufficient size to maintain temperatures suitable
         
     | 
| 
      
 60 
     | 
    
         
            +
              #       for HUMAN COMFORT:
         
     | 
| 
      
 61 
     | 
    
         
            +
              #         - COOLED: cooled by a system >= 10 W/m2
         
     | 
| 
      
 62 
     | 
    
         
            +
              #         - HEATED: heated by a system e.g., >= 50 W/m2 in Climate Zone CZ-7
         
     | 
| 
      
 63 
     | 
    
         
            +
              #         - INDIRECTLY: heated or cooled via adjacent space(s) provided:
         
     | 
| 
      
 64 
     | 
    
         
            +
              #             - UA of adjacent surfaces > UA of other surfaces
         
     | 
| 
      
 65 
     | 
    
         
            +
              #                 or
         
     | 
| 
      
 66 
     | 
    
         
            +
              #             - intentional air transfer from HEATED/COOLED space > 3 ACH
         
     | 
| 
      
 67 
     | 
    
         
            +
              #
         
     | 
| 
      
 68 
     | 
    
         
            +
              #               ... includes plenums, atria, etc.
         
     | 
| 
      
 69 
     | 
    
         
            +
              #
         
     | 
| 
      
 70 
     | 
    
         
            +
              #     - SEMI-HEATED space: an ENCLOSED space that has a heating system
         
     | 
| 
      
 71 
     | 
    
         
            +
              #       >= 10 W/m2, yet NOT a CONDITIONED space (see above).
         
     | 
| 
      
 72 
     | 
    
         
            +
              #
         
     | 
| 
      
 73 
     | 
    
         
            +
              #     - UNCONDITIONED space: an ENCLOSED space that is NOT a conditioned
         
     | 
| 
      
 74 
     | 
    
         
            +
              #       space or a SEMI-HEATED space (see above).
         
     | 
| 
      
 75 
     | 
    
         
            +
              #
         
     | 
| 
      
 76 
     | 
    
         
            +
              #       NOTE: Crawlspaces, attics, and parking garages with natural or
         
     | 
| 
      
 77 
     | 
    
         
            +
              #       mechanical ventilation are considered UNENCLOSED spaces.
         
     | 
| 
      
 78 
     | 
    
         
            +
              #
         
     | 
| 
      
 79 
     | 
    
         
            +
              #       2.3.3 Modeling Requirements: surfaces adjacent to UNENCLOSED spaces
         
     | 
| 
      
 80 
     | 
    
         
            +
              #       shall be treated as exterior surfaces. All other UNENCLOSED surfaces
         
     | 
| 
      
 81 
     | 
    
         
            +
              #       are to be modeled as is in both proposed and baseline models. For
         
     | 
| 
      
 82 
     | 
    
         
            +
              #       instance, modeled fenestration in UNENCLOSED spaces would not be
         
     | 
| 
      
 83 
     | 
    
         
            +
              #       factored in WWR calculations.
         
     | 
| 
      
 84 
     | 
    
         
            +
              #
         
     | 
| 
      
 85 
     | 
    
         
            +
              #
         
     | 
| 
      
 86 
     | 
    
         
            +
              # Related NECB definitions and concepts, starting with CONDITIONED space:
         
     | 
| 
      
 87 
     | 
    
         
            +
              #
         
     | 
| 
      
 88 
     | 
    
         
            +
              # "[...] the temperature of which is controlled to limit variation in
         
     | 
| 
      
 89 
     | 
    
         
            +
              # response to the exterior ambient temperature by the provision, either
         
     | 
| 
      
 90 
     | 
    
         
            +
              # DIRECTLY or INDIRECTLY, of heating or cooling [...]". Although criteria
         
     | 
| 
      
 91 
     | 
    
         
            +
              # differ (e.g., not sizing-based), the general idea is sufficiently similar
         
     | 
| 
      
 92 
     | 
    
         
            +
              # to ASHRAE 90.1 (e.g., heating and/or cooling based, no distinction for
         
     | 
| 
      
 93 
     | 
    
         
            +
              # INDIRECTLY conditioned spaces like plenums).
         
     | 
| 
      
 94 
     | 
    
         
            +
              #
         
     | 
| 
      
 95 
     | 
    
         
            +
              # SEMI-HEATED spaces are also a defined NECB term, but again the distinction
         
     | 
| 
      
 96 
     | 
    
         
            +
              # is based on desired/intended design space setpoint temperatures - not
         
     | 
| 
      
 97 
     | 
    
         
            +
              # system sizing criteria. No further treatment is implemented here to
         
     | 
| 
      
 98 
     | 
    
         
            +
              # distinguish SEMI-HEATED from CONDITIONED spaces.
         
     | 
| 
      
 99 
     | 
    
         
            +
              #
         
     | 
| 
      
 100 
     | 
    
         
            +
              # The single NECB criterion distinguishing UNCONDITIONED ENCLOSED spaces
         
     | 
| 
      
 101 
     | 
    
         
            +
              # (such as vestibules) from UNENCLOSED spaces (such as attics) remains the
         
     | 
| 
      
 102 
     | 
    
         
            +
              # intention to ventilate - or rather to what degree. Regardless, the methods
         
     | 
| 
      
 103 
     | 
    
         
            +
              # here are designed to process both classifications in the same way, namely by
         
     | 
| 
      
 104 
     | 
    
         
            +
              # focusing on adjacent surfaces to CONDITIONED (or SEMI-HEATED) spaces as part
         
     | 
| 
      
 105 
     | 
    
         
            +
              # of the building envelope.
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
              # In light of the above, methods here are designed without a priori knowledge
         
     | 
| 
      
 108 
     | 
    
         
            +
              # of explicit system sizing choices or access to iterative autosizing
         
     | 
| 
      
 109 
     | 
    
         
            +
              # processes. As discussed in greater detail elswhere, methods are developed to
         
     | 
| 
      
 110 
     | 
    
         
            +
              # rely on zoning info and/or "intended" temperature setpoints.
         
     | 
| 
      
 111 
     | 
    
         
            +
              #
         
     | 
| 
      
 112 
     | 
    
         
            +
              # For an OpenStudio model (OSM) in an incomplete or preliminary state, e.g.
         
     | 
| 
      
 113 
     | 
    
         
            +
              # holding fully-formed ENCLOSED spaces without thermal zoning information or
         
     | 
| 
      
 114 
     | 
    
         
            +
              # setpoint temperatures (early design stage assessments of form, porosity or
         
     | 
| 
      
 115 
     | 
    
         
            +
              # envelope), all OSM spaces will be considered CONDITIONED, presuming
         
     | 
| 
      
 116 
     | 
    
         
            +
              # setpoints of ~21°C (heating) and ~24°C (cooling).
         
     | 
| 
      
 117 
     | 
    
         
            +
              #
         
     | 
| 
      
 118 
     | 
    
         
            +
              # If ANY valid space/zone-specific temperature setpoints are found in the OSM,
         
     | 
| 
      
 119 
     | 
    
         
            +
              # spaces/zones WITHOUT valid heating or cooling setpoints are considered as
         
     | 
| 
      
 120 
     | 
    
         
            +
              # UNCONDITIONED or UNENCLOSED spaces (like attics), or INDIRECTLY CONDITIONED
         
     | 
| 
      
 121 
     | 
    
         
            +
              # spaces (like plenums), see "plenum?" method.
         
     | 
| 
      
 122 
     | 
    
         
            +
             
     | 
| 
      
 123 
     | 
    
         
            +
              ##
         
     | 
| 
      
 124 
     | 
    
         
            +
              # Return min & max values of a schedule (ruleset).
         
     | 
| 
      
 125 
     | 
    
         
            +
              #
         
     | 
| 
      
 126 
     | 
    
         
            +
              # @param sched [OpenStudio::Model::ScheduleRuleset] schedule
         
     | 
| 
      
 127 
     | 
    
         
            +
              #
         
     | 
| 
      
 128 
     | 
    
         
            +
              # @return [Hash] min: (Float), max: (Float)
         
     | 
| 
      
 129 
     | 
    
         
            +
              # @return [Hash] min: nil, max: nil (if invalid input)
         
     | 
| 
      
 130 
     | 
    
         
            +
              def scheduleRulesetMinMax(sched)
         
     | 
| 
      
 131 
     | 
    
         
            +
                # Largely inspired from David Goldwasser's
         
     | 
| 
      
 132 
     | 
    
         
            +
                # "schedule_ruleset_annual_min_max_value":
         
     | 
| 
      
 133 
     | 
    
         
            +
                #
         
     | 
| 
      
 134 
     | 
    
         
            +
                # github.com/NREL/openstudio-standards/blob/
         
     | 
| 
      
 135 
     | 
    
         
            +
                # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
         
     | 
| 
      
 136 
     | 
    
         
            +
                # standards/Standards.ScheduleRuleset.rb#L124
         
     | 
| 
      
 137 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 138 
     | 
    
         
            +
                cl  = OpenStudio::Model::ScheduleRuleset
         
     | 
| 
      
 139 
     | 
    
         
            +
                res = { min: nil, max: nil }
         
     | 
| 
      
 140 
     | 
    
         
            +
             
     | 
| 
      
 141 
     | 
    
         
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         
     | 
| 
      
 142 
     | 
    
         
            +
                id = sched.nameString
         
     | 
| 
      
 143 
     | 
    
         
            +
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         
     | 
| 
      
 144 
     | 
    
         
            +
             
     | 
| 
      
 145 
     | 
    
         
            +
                profiles = []
         
     | 
| 
      
 146 
     | 
    
         
            +
                profiles << sched.defaultDaySchedule
         
     | 
| 
      
 147 
     | 
    
         
            +
                sched.scheduleRules.each { |rule| profiles << rule.daySchedule }
         
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
                profiles.each do |profile|
         
     | 
| 
      
 150 
     | 
    
         
            +
                  id = profile.nameString
         
     | 
| 
      
 151 
     | 
    
         
            +
                  profile.values.each do |val|
         
     | 
| 
      
 152 
     | 
    
         
            +
                    unless val.is_a?(Numeric)
         
     | 
| 
      
 153 
     | 
    
         
            +
                      log(WRN, "Skipping non-numeric profile values in '#{id}' (#{mth})")
         
     | 
| 
      
 154 
     | 
    
         
            +
                      next
         
     | 
| 
      
 155 
     | 
    
         
            +
                    end
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
                    res[:min] = val unless res[:min]
         
     | 
| 
      
 158 
     | 
    
         
            +
                    res[:min] = val     if res[:min] > val
         
     | 
| 
      
 159 
     | 
    
         
            +
                    res[:max] = val unless res[:max]
         
     | 
| 
      
 160 
     | 
    
         
            +
                    res[:max] = val     if res[:max] < val
         
     | 
| 
      
 161 
     | 
    
         
            +
                  end
         
     | 
| 
      
 162 
     | 
    
         
            +
                end
         
     | 
| 
      
 163 
     | 
    
         
            +
             
     | 
| 
      
 164 
     | 
    
         
            +
                valid = res[:min] && res[:max]
         
     | 
| 
      
 165 
     | 
    
         
            +
                log(ERR, "Invalid MIN/MAX in '#{id}' (#{mth})") unless valid
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                res
         
     | 
| 
      
 168 
     | 
    
         
            +
              end
         
     | 
| 
      
 169 
     | 
    
         
            +
             
     | 
| 
      
 170 
     | 
    
         
            +
              ##
         
     | 
| 
      
 171 
     | 
    
         
            +
              # Return min & max values of a schedule (constant).
         
     | 
| 
      
 172 
     | 
    
         
            +
              #
         
     | 
| 
      
 173 
     | 
    
         
            +
              # @param sched [OpenStudio::Model::ScheduleConstant] schedule
         
     | 
| 
      
 174 
     | 
    
         
            +
              #
         
     | 
| 
      
 175 
     | 
    
         
            +
              # @return [Hash] min: (Float), max: (Float)
         
     | 
| 
      
 176 
     | 
    
         
            +
              # @return [Hash] min: nil, max: nil (if invalid input)
         
     | 
| 
      
 177 
     | 
    
         
            +
              def scheduleConstantMinMax(sched)
         
     | 
| 
      
 178 
     | 
    
         
            +
                # Largely inspired from David Goldwasser's
         
     | 
| 
      
 179 
     | 
    
         
            +
                # "schedule_constant_annual_min_max_value":
         
     | 
| 
      
 180 
     | 
    
         
            +
                #
         
     | 
| 
      
 181 
     | 
    
         
            +
                # github.com/NREL/openstudio-standards/blob/
         
     | 
| 
      
 182 
     | 
    
         
            +
                # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
         
     | 
| 
      
 183 
     | 
    
         
            +
                # standards/Standards.ScheduleConstant.rb#L21
         
     | 
| 
      
 184 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 185 
     | 
    
         
            +
                cl  = OpenStudio::Model::ScheduleConstant
         
     | 
| 
      
 186 
     | 
    
         
            +
                res = { min: nil, max: nil }
         
     | 
| 
      
 187 
     | 
    
         
            +
             
     | 
| 
      
 188 
     | 
    
         
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         
     | 
| 
      
 189 
     | 
    
         
            +
                id = sched.nameString
         
     | 
| 
      
 190 
     | 
    
         
            +
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         
     | 
| 
      
 191 
     | 
    
         
            +
             
     | 
| 
      
 192 
     | 
    
         
            +
                unless sched.value.is_a?(Numeric)
         
     | 
| 
      
 193 
     | 
    
         
            +
                  return mismatch("'#{id}' value", sched.value, Numeric, mth, ERR, res)
         
     | 
| 
      
 194 
     | 
    
         
            +
                else
         
     | 
| 
      
 195 
     | 
    
         
            +
                  res[:min] = sched.value
         
     | 
| 
      
 196 
     | 
    
         
            +
                  res[:max] = sched.value
         
     | 
| 
      
 197 
     | 
    
         
            +
                end
         
     | 
| 
      
 198 
     | 
    
         
            +
             
     | 
| 
      
 199 
     | 
    
         
            +
                res
         
     | 
| 
      
 200 
     | 
    
         
            +
              end
         
     | 
| 
      
 201 
     | 
    
         
            +
             
     | 
| 
      
 202 
     | 
    
         
            +
              ##
         
     | 
| 
      
 203 
     | 
    
         
            +
              # Return min & max values of a schedule (compact).
         
     | 
| 
      
 204 
     | 
    
         
            +
              #
         
     | 
| 
      
 205 
     | 
    
         
            +
              # @param sched [OpenStudio::Model::ScheduleCompact] schedule
         
     | 
| 
      
 206 
     | 
    
         
            +
              #
         
     | 
| 
      
 207 
     | 
    
         
            +
              # @return [Hash] min: (Float), max: (Float)
         
     | 
| 
      
 208 
     | 
    
         
            +
              # @return [Hash] min: nil, max: nil (if invalid input)
         
     | 
| 
      
 209 
     | 
    
         
            +
              def scheduleCompactMinMax(sched)
         
     | 
| 
      
 210 
     | 
    
         
            +
                # Largely inspired from Andrew Parker's
         
     | 
| 
      
 211 
     | 
    
         
            +
                # "schedule_compact_annual_min_max_value":
         
     | 
| 
      
 212 
     | 
    
         
            +
                #
         
     | 
| 
      
 213 
     | 
    
         
            +
                # github.com/NREL/openstudio-standards/blob/
         
     | 
| 
      
 214 
     | 
    
         
            +
                # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
         
     | 
| 
      
 215 
     | 
    
         
            +
                # standards/Standards.ScheduleCompact.rb#L8
         
     | 
| 
      
 216 
     | 
    
         
            +
                mth      = "OSut::#{__callee__}"
         
     | 
| 
      
 217 
     | 
    
         
            +
                cl       = OpenStudio::Model::ScheduleCompact
         
     | 
| 
      
 218 
     | 
    
         
            +
                vals     = []
         
     | 
| 
      
 219 
     | 
    
         
            +
                prev_str = ""
         
     | 
| 
      
 220 
     | 
    
         
            +
                res      = { min: nil, max: nil }
         
     | 
| 
      
 221 
     | 
    
         
            +
             
     | 
| 
      
 222 
     | 
    
         
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         
     | 
| 
      
 223 
     | 
    
         
            +
                id = sched.nameString
         
     | 
| 
      
 224 
     | 
    
         
            +
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         
     | 
| 
      
 225 
     | 
    
         
            +
             
     | 
| 
      
 226 
     | 
    
         
            +
                sched.extensibleGroups.each do |eg|
         
     | 
| 
      
 227 
     | 
    
         
            +
                  if prev_str.include?("until")
         
     | 
| 
      
 228 
     | 
    
         
            +
                    vals << eg.getDouble(0).get unless eg.getDouble(0).empty?
         
     | 
| 
      
 229 
     | 
    
         
            +
                  end
         
     | 
| 
      
 230 
     | 
    
         
            +
             
     | 
| 
      
 231 
     | 
    
         
            +
                  str = eg.getString(0)
         
     | 
| 
      
 232 
     | 
    
         
            +
                  prev_str = str.get.downcase unless str.empty?
         
     | 
| 
      
 233 
     | 
    
         
            +
                end
         
     | 
| 
      
 234 
     | 
    
         
            +
             
     | 
| 
      
 235 
     | 
    
         
            +
                return empty("'#{id}' values", mth, ERR, res) if vals.empty?
         
     | 
| 
      
 236 
     | 
    
         
            +
             
     | 
| 
      
 237 
     | 
    
         
            +
                if vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
         
     | 
| 
      
 238 
     | 
    
         
            +
                  res[:min] = vals.min
         
     | 
| 
      
 239 
     | 
    
         
            +
                  res[:max] = vals.max
         
     | 
| 
      
 240 
     | 
    
         
            +
                else
         
     | 
| 
      
 241 
     | 
    
         
            +
                  log(ERR, "Non-numeric values in '#{id}' (#{mth})")
         
     | 
| 
      
 242 
     | 
    
         
            +
                end
         
     | 
| 
      
 243 
     | 
    
         
            +
             
     | 
| 
      
 244 
     | 
    
         
            +
                res
         
     | 
| 
      
 245 
     | 
    
         
            +
              end
         
     | 
| 
      
 246 
     | 
    
         
            +
             
     | 
| 
      
 247 
     | 
    
         
            +
              ##
         
     | 
| 
      
 248 
     | 
    
         
            +
              # Return min & max values for schedule (interval).
         
     | 
| 
      
 249 
     | 
    
         
            +
              #
         
     | 
| 
      
 250 
     | 
    
         
            +
              # @param sched [OpenStudio::Model::ScheduleInterval] schedule
         
     | 
| 
      
 251 
     | 
    
         
            +
              #
         
     | 
| 
      
 252 
     | 
    
         
            +
              # @return [Hash] min: (Float), max: (Float)
         
     | 
| 
      
 253 
     | 
    
         
            +
              # @return [Hash] min: nil, max: nil (if invalid input)
         
     | 
| 
      
 254 
     | 
    
         
            +
              def scheduleIntervalMinMax(sched)
         
     | 
| 
      
 255 
     | 
    
         
            +
                mth      = "OSut::#{__callee__}"
         
     | 
| 
      
 256 
     | 
    
         
            +
                cl       = OpenStudio::Model::ScheduleInterval
         
     | 
| 
      
 257 
     | 
    
         
            +
                vals     = []
         
     | 
| 
      
 258 
     | 
    
         
            +
                prev_str = ""
         
     | 
| 
      
 259 
     | 
    
         
            +
                res      = { min: nil, max: nil }
         
     | 
| 
      
 260 
     | 
    
         
            +
             
     | 
| 
      
 261 
     | 
    
         
            +
                return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS)
         
     | 
| 
      
 262 
     | 
    
         
            +
                id = sched.nameString
         
     | 
| 
      
 263 
     | 
    
         
            +
                return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl)
         
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
      
 265 
     | 
    
         
            +
                vals = sched.timeSeries.values
         
     | 
| 
      
 266 
     | 
    
         
            +
                if vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric)
         
     | 
| 
      
 267 
     | 
    
         
            +
                  res[:min] = vals.min
         
     | 
| 
      
 268 
     | 
    
         
            +
                  res[:max] = vals.max
         
     | 
| 
      
 269 
     | 
    
         
            +
                else
         
     | 
| 
      
 270 
     | 
    
         
            +
                  log(ERR, "Non-numeric values in '#{id}' (#{mth})")
         
     | 
| 
      
 271 
     | 
    
         
            +
                end
         
     | 
| 
      
 272 
     | 
    
         
            +
             
     | 
| 
      
 273 
     | 
    
         
            +
                res
         
     | 
| 
      
 274 
     | 
    
         
            +
              end
         
     | 
| 
      
 275 
     | 
    
         
            +
             
     | 
| 
      
 276 
     | 
    
         
            +
              ##
         
     | 
| 
      
 277 
     | 
    
         
            +
              # Return max zone heating temperature schedule setpoint [°C] and whether
         
     | 
| 
      
 278 
     | 
    
         
            +
              # zone has active dual setpoint thermostat.
         
     | 
| 
      
 279 
     | 
    
         
            +
              #
         
     | 
| 
      
 280 
     | 
    
         
            +
              # @param zone [OpenStudio::Model::ThermalZone] a thermal zone
         
     | 
| 
      
 281 
     | 
    
         
            +
              #
         
     | 
| 
      
 282 
     | 
    
         
            +
              # @return [Hash] spt: (Float), dual: (Bool)
         
     | 
| 
      
 283 
     | 
    
         
            +
              # @return [Hash] spt: nil, dual: false (if invalid input)
         
     | 
| 
      
 284 
     | 
    
         
            +
              def maxHeatScheduledSetpoint(zone)
         
     | 
| 
      
 285 
     | 
    
         
            +
                # Largely inspired from Parker & Marrec's "thermal_zone_heated?" procedure.
         
     | 
| 
      
 286 
     | 
    
         
            +
                # The solution here is a tad more relaxed to encompass SEMI-HEATED zones as
         
     | 
| 
      
 287 
     | 
    
         
            +
                # per Canadian NECB criteria (basically any space with at least 10 W/m2 of
         
     | 
| 
      
 288 
     | 
    
         
            +
                # installed heating equipement, i.e. below freezing in Canada).
         
     | 
| 
      
 289 
     | 
    
         
            +
                #
         
     | 
| 
      
 290 
     | 
    
         
            +
                # github.com/NREL/openstudio-standards/blob/
         
     | 
| 
      
 291 
     | 
    
         
            +
                # 58964222d25783e9da4ae292e375fb0d5c902aa5/lib/openstudio-standards/
         
     | 
| 
      
 292 
     | 
    
         
            +
                # standards/Standards.ThermalZone.rb#L910
         
     | 
| 
      
 293 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 294 
     | 
    
         
            +
                cl  = OpenStudio::Model::ThermalZone
         
     | 
| 
      
 295 
     | 
    
         
            +
                res = { spt: nil, dual: false }
         
     | 
| 
      
 296 
     | 
    
         
            +
             
     | 
| 
      
 297 
     | 
    
         
            +
                return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
         
     | 
| 
      
 298 
     | 
    
         
            +
                id = zone.nameString
         
     | 
| 
      
 299 
     | 
    
         
            +
                return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
         
     | 
| 
      
 300 
     | 
    
         
            +
             
     | 
| 
      
 301 
     | 
    
         
            +
                # Zone radiant heating? Get schedule from radiant system.
         
     | 
| 
      
 302 
     | 
    
         
            +
                zone.equipment.each do |equip|
         
     | 
| 
      
 303 
     | 
    
         
            +
                  sched = nil
         
     | 
| 
      
 304 
     | 
    
         
            +
             
     | 
| 
      
 305 
     | 
    
         
            +
                  unless equip.to_ZoneHVACHighTemperatureRadiant.empty?
         
     | 
| 
      
 306 
     | 
    
         
            +
                    equip = equip.to_ZoneHVACHighTemperatureRadiant.get
         
     | 
| 
      
 307 
     | 
    
         
            +
             
     | 
| 
      
 308 
     | 
    
         
            +
                    unless equip.heatingSetpointTemperatureSchedule.empty?
         
     | 
| 
      
 309 
     | 
    
         
            +
                      sched = equip.heatingSetpointTemperatureSchedule.get
         
     | 
| 
      
 310 
     | 
    
         
            +
                    end
         
     | 
| 
      
 311 
     | 
    
         
            +
                  end
         
     | 
| 
      
 312 
     | 
    
         
            +
             
     | 
| 
      
 313 
     | 
    
         
            +
                  unless equip.to_ZoneHVACLowTemperatureRadiantElectric.empty?
         
     | 
| 
      
 314 
     | 
    
         
            +
                    equip = equip.to_ZoneHVACLowTemperatureRadiantElectric.get
         
     | 
| 
      
 315 
     | 
    
         
            +
             
     | 
| 
      
 316 
     | 
    
         
            +
                    unless equip.heatingSetpointTemperatureSchedule.empty?
         
     | 
| 
      
 317 
     | 
    
         
            +
                      sched = equip.heatingSetpointTemperatureSchedule.get
         
     | 
| 
      
 318 
     | 
    
         
            +
                    end
         
     | 
| 
      
 319 
     | 
    
         
            +
                  end
         
     | 
| 
      
 320 
     | 
    
         
            +
             
     | 
| 
      
 321 
     | 
    
         
            +
                  unless equip.to_ZoneHVACLowTempRadiantConstFlow.empty?
         
     | 
| 
      
 322 
     | 
    
         
            +
                    equip = equip.to_ZoneHVACLowTempRadiantConstFlow.get
         
     | 
| 
      
 323 
     | 
    
         
            +
                    coil = equip.heatingCoil
         
     | 
| 
      
 324 
     | 
    
         
            +
             
     | 
| 
      
 325 
     | 
    
         
            +
                    unless coil.to_CoilHeatingLowTempRadiantConstFlow.empty?
         
     | 
| 
      
 326 
     | 
    
         
            +
                      coil = coil.to_CoilHeatingLowTempRadiantConstFlow.get
         
     | 
| 
      
 327 
     | 
    
         
            +
             
     | 
| 
      
 328 
     | 
    
         
            +
                      unless coil.heatingHighControlTemperatureSchedule.empty?
         
     | 
| 
      
 329 
     | 
    
         
            +
                        sched = c.heatingHighControlTemperatureSchedule.get
         
     | 
| 
      
 330 
     | 
    
         
            +
                      end
         
     | 
| 
      
 331 
     | 
    
         
            +
                    end
         
     | 
| 
      
 332 
     | 
    
         
            +
                  end
         
     | 
| 
      
 333 
     | 
    
         
            +
             
     | 
| 
      
 334 
     | 
    
         
            +
                  unless equip.to_ZoneHVACLowTempRadiantVarFlow.empty?
         
     | 
| 
      
 335 
     | 
    
         
            +
                    equip = equip.to_ZoneHVACLowTempRadiantVarFlow.get
         
     | 
| 
      
 336 
     | 
    
         
            +
                    coil = equip.heatingCoil
         
     | 
| 
      
 337 
     | 
    
         
            +
             
     | 
| 
      
 338 
     | 
    
         
            +
                    unless coil.to_CoilHeatingLowTempRadiantVarFlow.empty?
         
     | 
| 
      
 339 
     | 
    
         
            +
                      coil = coil.to_CoilHeatingLowTempRadiantVarFlow.get
         
     | 
| 
      
 340 
     | 
    
         
            +
             
     | 
| 
      
 341 
     | 
    
         
            +
                      unless coil.heatingControlTemperatureSchedule.empty?
         
     | 
| 
      
 342 
     | 
    
         
            +
                        sched = coil.heatingControlTemperatureSchedule.get
         
     | 
| 
      
 343 
     | 
    
         
            +
                      end
         
     | 
| 
      
 344 
     | 
    
         
            +
                    end
         
     | 
| 
      
 345 
     | 
    
         
            +
                  end
         
     | 
| 
      
 346 
     | 
    
         
            +
             
     | 
| 
      
 347 
     | 
    
         
            +
                  next unless sched
         
     | 
| 
      
 348 
     | 
    
         
            +
             
     | 
| 
      
 349 
     | 
    
         
            +
                  unless sched.to_ScheduleRuleset.empty?
         
     | 
| 
      
 350 
     | 
    
         
            +
                    sched = sched.to_ScheduleRuleset.get
         
     | 
| 
      
 351 
     | 
    
         
            +
                    max = scheduleRulesetMinMax(sched)[:max]
         
     | 
| 
      
 352 
     | 
    
         
            +
             
     | 
| 
      
 353 
     | 
    
         
            +
                    if max
         
     | 
| 
      
 354 
     | 
    
         
            +
                      res[:spt] = max unless res[:spt]
         
     | 
| 
      
 355 
     | 
    
         
            +
                      res[:spt] = max     if res[:spt] < max
         
     | 
| 
      
 356 
     | 
    
         
            +
                    end
         
     | 
| 
      
 357 
     | 
    
         
            +
                  end
         
     | 
| 
      
 358 
     | 
    
         
            +
             
     | 
| 
      
 359 
     | 
    
         
            +
                  unless sched.to_ScheduleConstant.empty?
         
     | 
| 
      
 360 
     | 
    
         
            +
                    sched = sched.to_ScheduleConstant.get
         
     | 
| 
      
 361 
     | 
    
         
            +
                    max = scheduleConstantMinMax(sched)[:max]
         
     | 
| 
      
 362 
     | 
    
         
            +
             
     | 
| 
      
 363 
     | 
    
         
            +
                    if max
         
     | 
| 
      
 364 
     | 
    
         
            +
                      res[:spt] = max unless res[:spt]
         
     | 
| 
      
 365 
     | 
    
         
            +
                      res[:spt] = max     if res[:spt] < max
         
     | 
| 
      
 366 
     | 
    
         
            +
                    end
         
     | 
| 
      
 367 
     | 
    
         
            +
                  end
         
     | 
| 
      
 368 
     | 
    
         
            +
             
     | 
| 
      
 369 
     | 
    
         
            +
                  unless sched.to_ScheduleCompact.empty?
         
     | 
| 
      
 370 
     | 
    
         
            +
                    sched = sched.to_ScheduleCompact.get
         
     | 
| 
      
 371 
     | 
    
         
            +
                    max = scheduleCompactMinMax(sched)[:max]
         
     | 
| 
      
 372 
     | 
    
         
            +
             
     | 
| 
      
 373 
     | 
    
         
            +
                    if max
         
     | 
| 
      
 374 
     | 
    
         
            +
                      res[:spt] = max unless res[:spt]
         
     | 
| 
      
 375 
     | 
    
         
            +
                      res[:spt] = max     if res[:spt] < max
         
     | 
| 
      
 376 
     | 
    
         
            +
                    end
         
     | 
| 
      
 377 
     | 
    
         
            +
                  end
         
     | 
| 
      
 378 
     | 
    
         
            +
                end
         
     | 
| 
      
 379 
     | 
    
         
            +
             
     | 
| 
      
 380 
     | 
    
         
            +
                return res if res[:spt]
         
     | 
| 
      
 381 
     | 
    
         
            +
                return res if zone.thermostat.empty?
         
     | 
| 
      
 382 
     | 
    
         
            +
                tstat = zone.thermostat.get
         
     | 
| 
      
 383 
     | 
    
         
            +
             
     | 
| 
      
 384 
     | 
    
         
            +
                unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
         
     | 
| 
      
 385 
     | 
    
         
            +
                       tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
         
     | 
| 
      
 386 
     | 
    
         
            +
                  res[:dual] = true
         
     | 
| 
      
 387 
     | 
    
         
            +
             
     | 
| 
      
 388 
     | 
    
         
            +
                  unless tstat.to_ThermostatSetpointDualSetpoint.empty?
         
     | 
| 
      
 389 
     | 
    
         
            +
                    tstat = tstat.to_ThermostatSetpointDualSetpoint.get
         
     | 
| 
      
 390 
     | 
    
         
            +
                  else
         
     | 
| 
      
 391 
     | 
    
         
            +
                    tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get
         
     | 
| 
      
 392 
     | 
    
         
            +
                  end
         
     | 
| 
      
 393 
     | 
    
         
            +
             
     | 
| 
      
 394 
     | 
    
         
            +
                  unless tstat.heatingSetpointTemperatureSchedule.empty?
         
     | 
| 
      
 395 
     | 
    
         
            +
                    sched = tstat.heatingSetpointTemperatureSchedule.get
         
     | 
| 
      
 396 
     | 
    
         
            +
             
     | 
| 
      
 397 
     | 
    
         
            +
                    unless sched.to_ScheduleRuleset.empty?
         
     | 
| 
      
 398 
     | 
    
         
            +
                      sched = sched.to_ScheduleRuleset.get
         
     | 
| 
      
 399 
     | 
    
         
            +
                      max = scheduleRulesetMinMax(sched)[:max]
         
     | 
| 
      
 400 
     | 
    
         
            +
             
     | 
| 
      
 401 
     | 
    
         
            +
                      if max
         
     | 
| 
      
 402 
     | 
    
         
            +
                        res[:spt] = max unless res[:spt]
         
     | 
| 
      
 403 
     | 
    
         
            +
                        res[:spt] = max     if res[:spt] < max
         
     | 
| 
      
 404 
     | 
    
         
            +
                      end
         
     | 
| 
      
 405 
     | 
    
         
            +
             
     | 
| 
      
 406 
     | 
    
         
            +
                      dd = sched.winterDesignDaySchedule
         
     | 
| 
      
 407 
     | 
    
         
            +
             
     | 
| 
      
 408 
     | 
    
         
            +
                      unless dd.values.empty?
         
     | 
| 
      
 409 
     | 
    
         
            +
                        res[:spt] = dd.values.max unless res[:spt]
         
     | 
| 
      
 410 
     | 
    
         
            +
                        res[:spt] = dd.values.max     if res[:spt] < dd.values.max
         
     | 
| 
      
 411 
     | 
    
         
            +
                      end
         
     | 
| 
      
 412 
     | 
    
         
            +
                    end
         
     | 
| 
      
 413 
     | 
    
         
            +
             
     | 
| 
      
 414 
     | 
    
         
            +
                    unless sched.to_ScheduleConstant.empty?
         
     | 
| 
      
 415 
     | 
    
         
            +
                      sched = sched.to_ScheduleConstant.get
         
     | 
| 
      
 416 
     | 
    
         
            +
                      max = scheduleConstantMinMax(sched)[:max]
         
     | 
| 
      
 417 
     | 
    
         
            +
             
     | 
| 
      
 418 
     | 
    
         
            +
                      if max
         
     | 
| 
      
 419 
     | 
    
         
            +
                        res[:spt] = max unless res[:spt]
         
     | 
| 
      
 420 
     | 
    
         
            +
                        res[:spt] = max     if res[:spt] < max
         
     | 
| 
      
 421 
     | 
    
         
            +
                      end
         
     | 
| 
      
 422 
     | 
    
         
            +
                    end
         
     | 
| 
      
 423 
     | 
    
         
            +
             
     | 
| 
      
 424 
     | 
    
         
            +
                    unless sched.to_ScheduleCompact.empty?
         
     | 
| 
      
 425 
     | 
    
         
            +
                      sched = sched.to_ScheduleCompact.get
         
     | 
| 
      
 426 
     | 
    
         
            +
                      max = scheduleCompactMinMax(sched)[:max]
         
     | 
| 
      
 427 
     | 
    
         
            +
             
     | 
| 
      
 428 
     | 
    
         
            +
                      if max
         
     | 
| 
      
 429 
     | 
    
         
            +
                        res[:spt] = max unless res[:spt]
         
     | 
| 
      
 430 
     | 
    
         
            +
                        res[:spt] = max     if res[:spt] < max
         
     | 
| 
      
 431 
     | 
    
         
            +
                      end
         
     | 
| 
      
 432 
     | 
    
         
            +
                    end
         
     | 
| 
      
 433 
     | 
    
         
            +
             
     | 
| 
      
 434 
     | 
    
         
            +
                    unless sched.to_ScheduleYear.empty?
         
     | 
| 
      
 435 
     | 
    
         
            +
                      sched = sched.to_ScheduleYear.get
         
     | 
| 
      
 436 
     | 
    
         
            +
             
     | 
| 
      
 437 
     | 
    
         
            +
                      sched.getScheduleWeeks.each do |week|
         
     | 
| 
      
 438 
     | 
    
         
            +
                        next if week.winterDesignDaySchedule.empty?
         
     | 
| 
      
 439 
     | 
    
         
            +
                        dd = week.winterDesignDaySchedule.get
         
     | 
| 
      
 440 
     | 
    
         
            +
                        next unless dd.values.empty?
         
     | 
| 
      
 441 
     | 
    
         
            +
             
     | 
| 
      
 442 
     | 
    
         
            +
                        res[:spt] = dd.values.max unless res[:spt]
         
     | 
| 
      
 443 
     | 
    
         
            +
                        res[:spt] = dd.values.max     if res[:spt] < dd.values.max
         
     | 
| 
      
 444 
     | 
    
         
            +
                      end
         
     | 
| 
      
 445 
     | 
    
         
            +
                    end
         
     | 
| 
      
 446 
     | 
    
         
            +
                  end
         
     | 
| 
      
 447 
     | 
    
         
            +
                end
         
     | 
| 
      
 448 
     | 
    
         
            +
             
     | 
| 
      
 449 
     | 
    
         
            +
                res
         
     | 
| 
      
 450 
     | 
    
         
            +
              end
         
     | 
| 
      
 451 
     | 
    
         
            +
             
     | 
| 
      
 452 
     | 
    
         
            +
              ##
         
     | 
| 
      
 453 
     | 
    
         
            +
              # Validate if model has zones with valid heating temperature setpoints.
         
     | 
| 
      
 454 
     | 
    
         
            +
              #
         
     | 
| 
      
 455 
     | 
    
         
            +
              # @param model [OpenStudio::Model::Model] a model
         
     | 
| 
      
 456 
     | 
    
         
            +
              #
         
     | 
| 
      
 457 
     | 
    
         
            +
              # @return [Bool] true if valid heating temperature setpoints
         
     | 
| 
      
 458 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 459 
     | 
    
         
            +
              def heatingTemperatureSetpoints?(model)
         
     | 
| 
      
 460 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 461 
     | 
    
         
            +
                cl = OpenStudio::Model::Model
         
     | 
| 
      
 462 
     | 
    
         
            +
             
     | 
| 
      
 463 
     | 
    
         
            +
                return invalid("model", mth, 1, DBG, false) unless model
         
     | 
| 
      
 464 
     | 
    
         
            +
                return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
         
     | 
| 
      
 465 
     | 
    
         
            +
             
     | 
| 
      
 466 
     | 
    
         
            +
                model.getThermalZones.each do |zone|
         
     | 
| 
      
 467 
     | 
    
         
            +
                  return true if maxHeatScheduledSetpoint(zone)[:spt]
         
     | 
| 
      
 468 
     | 
    
         
            +
                end
         
     | 
| 
      
 469 
     | 
    
         
            +
             
     | 
| 
      
 470 
     | 
    
         
            +
                false
         
     | 
| 
      
 471 
     | 
    
         
            +
              end
         
     | 
| 
      
 472 
     | 
    
         
            +
             
     | 
| 
      
 473 
     | 
    
         
            +
              ##
         
     | 
| 
      
 474 
     | 
    
         
            +
              # Return min zone cooling temperature schedule setpoint [°C] and whether
         
     | 
| 
      
 475 
     | 
    
         
            +
              # zone has active dual setpoint thermostat.
         
     | 
| 
      
 476 
     | 
    
         
            +
              #
         
     | 
| 
      
 477 
     | 
    
         
            +
              # @param zone [OpenStudio::Model::ThermalZone] a thermal zone
         
     | 
| 
      
 478 
     | 
    
         
            +
              #
         
     | 
| 
      
 479 
     | 
    
         
            +
              # @return [Hash] spt: (Float), dual: (Bool)
         
     | 
| 
      
 480 
     | 
    
         
            +
              # @return [Hash] spt: nil, dual: false (if invalid input)
         
     | 
| 
      
 481 
     | 
    
         
            +
              def minCoolScheduledSetpoint(zone)
         
     | 
| 
      
 482 
     | 
    
         
            +
                # Largely inspired from Parker & Marrec's "thermal_zone_cooled?" procedure.
         
     | 
| 
      
 483 
     | 
    
         
            +
                #
         
     | 
| 
      
 484 
     | 
    
         
            +
                # github.com/NREL/openstudio-standards/blob/
         
     | 
| 
      
 485 
     | 
    
         
            +
                # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/
         
     | 
| 
      
 486 
     | 
    
         
            +
                # standards/Standards.ThermalZone.rb#L1058
         
     | 
| 
      
 487 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 488 
     | 
    
         
            +
                cl  = OpenStudio::Model::ThermalZone
         
     | 
| 
      
 489 
     | 
    
         
            +
                res = { spt: nil, dual: false }
         
     | 
| 
      
 490 
     | 
    
         
            +
             
     | 
| 
      
 491 
     | 
    
         
            +
                return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS)
         
     | 
| 
      
 492 
     | 
    
         
            +
                id = zone.nameString
         
     | 
| 
      
 493 
     | 
    
         
            +
                return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl)
         
     | 
| 
      
 494 
     | 
    
         
            +
             
     | 
| 
      
 495 
     | 
    
         
            +
                # Zone radiant cooling? Get schedule from radiant system.
         
     | 
| 
      
 496 
     | 
    
         
            +
                zone.equipment.each do |equip|
         
     | 
| 
      
 497 
     | 
    
         
            +
                  sched = nil
         
     | 
| 
      
 498 
     | 
    
         
            +
             
     | 
| 
      
 499 
     | 
    
         
            +
                  unless equip.to_ZoneHVACLowTempRadiantConstFlow.empty?
         
     | 
| 
      
 500 
     | 
    
         
            +
                    equip = equip.to_ZoneHVACLowTempRadiantConstFlow.get
         
     | 
| 
      
 501 
     | 
    
         
            +
                    coil = equip.coolingCoil
         
     | 
| 
      
 502 
     | 
    
         
            +
             
     | 
| 
      
 503 
     | 
    
         
            +
                    unless coil.to_CoilCoolingLowTempRadiantConstFlow.empty?
         
     | 
| 
      
 504 
     | 
    
         
            +
                      coil = coil.to_CoilCoolingLowTempRadiantConstFlow.get
         
     | 
| 
      
 505 
     | 
    
         
            +
             
     | 
| 
      
 506 
     | 
    
         
            +
                      unless coil.coolingLowControlTemperatureSchedule.empty?
         
     | 
| 
      
 507 
     | 
    
         
            +
                        sched = coil.coolingLowControlTemperatureSchedule.get
         
     | 
| 
      
 508 
     | 
    
         
            +
                      end
         
     | 
| 
      
 509 
     | 
    
         
            +
                    end
         
     | 
| 
      
 510 
     | 
    
         
            +
                  end
         
     | 
| 
      
 511 
     | 
    
         
            +
             
     | 
| 
      
 512 
     | 
    
         
            +
                  unless equip.to_ZoneHVACLowTempRadiantVarFlow.empty?
         
     | 
| 
      
 513 
     | 
    
         
            +
                    equip = equip.to_ZoneHVACLowTempRadiantVarFlow.get
         
     | 
| 
      
 514 
     | 
    
         
            +
                    coil = equip.coolingCoil
         
     | 
| 
      
 515 
     | 
    
         
            +
             
     | 
| 
      
 516 
     | 
    
         
            +
                    unless coil.to_CoilCoolingLowTempRadiantVarFlow.empty?
         
     | 
| 
      
 517 
     | 
    
         
            +
                      coil = coil.to_CoilCoolingLowTempRadiantVarFlow.get
         
     | 
| 
      
 518 
     | 
    
         
            +
             
     | 
| 
      
 519 
     | 
    
         
            +
                      unless coil.coolingControlTemperatureSchedule.empty?
         
     | 
| 
      
 520 
     | 
    
         
            +
                        sched = coil.coolingControlTemperatureSchedule.get
         
     | 
| 
      
 521 
     | 
    
         
            +
                      end
         
     | 
| 
      
 522 
     | 
    
         
            +
                    end
         
     | 
| 
      
 523 
     | 
    
         
            +
                  end
         
     | 
| 
      
 524 
     | 
    
         
            +
             
     | 
| 
      
 525 
     | 
    
         
            +
                  next unless sched
         
     | 
| 
      
 526 
     | 
    
         
            +
             
     | 
| 
      
 527 
     | 
    
         
            +
                  unless sched.to_ScheduleRuleset.empty?
         
     | 
| 
      
 528 
     | 
    
         
            +
                    sched = sched.to_ScheduleRuleset.get
         
     | 
| 
      
 529 
     | 
    
         
            +
                    min = scheduleRulesetMinMax(sched)[:min]
         
     | 
| 
      
 530 
     | 
    
         
            +
             
     | 
| 
      
 531 
     | 
    
         
            +
                    if min
         
     | 
| 
      
 532 
     | 
    
         
            +
                      res[:spt] = min unless res[:spt]
         
     | 
| 
      
 533 
     | 
    
         
            +
                      res[:spt] = min     if res[:spt] > min
         
     | 
| 
      
 534 
     | 
    
         
            +
                    end
         
     | 
| 
      
 535 
     | 
    
         
            +
                  end
         
     | 
| 
      
 536 
     | 
    
         
            +
             
     | 
| 
      
 537 
     | 
    
         
            +
                  unless sched.to_ScheduleConstant.empty?
         
     | 
| 
      
 538 
     | 
    
         
            +
                    sched = sched.to_ScheduleConstant.get
         
     | 
| 
      
 539 
     | 
    
         
            +
                    min = scheduleConstantMinMax(sched)[:min]
         
     | 
| 
      
 540 
     | 
    
         
            +
             
     | 
| 
      
 541 
     | 
    
         
            +
                    if min
         
     | 
| 
      
 542 
     | 
    
         
            +
                      res[:spt] = min unless res[:spt]
         
     | 
| 
      
 543 
     | 
    
         
            +
                      res[:spt] = min     if res[:spt] > min
         
     | 
| 
      
 544 
     | 
    
         
            +
                    end
         
     | 
| 
      
 545 
     | 
    
         
            +
                  end
         
     | 
| 
      
 546 
     | 
    
         
            +
             
     | 
| 
      
 547 
     | 
    
         
            +
                  unless sched.to_ScheduleCompact.empty?
         
     | 
| 
      
 548 
     | 
    
         
            +
                    sched = sched.to_ScheduleCompact.get
         
     | 
| 
      
 549 
     | 
    
         
            +
                    min = scheduleCompactMinMax(sched)[:min]
         
     | 
| 
      
 550 
     | 
    
         
            +
             
     | 
| 
      
 551 
     | 
    
         
            +
                    if min
         
     | 
| 
      
 552 
     | 
    
         
            +
                      res[:spt] = min unless res[:spt]
         
     | 
| 
      
 553 
     | 
    
         
            +
                      res[:spt] = min     if res[:spt] > min
         
     | 
| 
      
 554 
     | 
    
         
            +
                    end
         
     | 
| 
      
 555 
     | 
    
         
            +
                  end
         
     | 
| 
      
 556 
     | 
    
         
            +
                end
         
     | 
| 
      
 557 
     | 
    
         
            +
             
     | 
| 
      
 558 
     | 
    
         
            +
                return res if res[:spt]
         
     | 
| 
      
 559 
     | 
    
         
            +
                return res if zone.thermostat.empty?
         
     | 
| 
      
 560 
     | 
    
         
            +
                tstat = zone.thermostat.get
         
     | 
| 
      
 561 
     | 
    
         
            +
             
     | 
| 
      
 562 
     | 
    
         
            +
                unless tstat.to_ThermostatSetpointDualSetpoint.empty? &&
         
     | 
| 
      
 563 
     | 
    
         
            +
                       tstat.to_ZoneControlThermostatStagedDualSetpoint.empty?
         
     | 
| 
      
 564 
     | 
    
         
            +
                  res[:dual] = true
         
     | 
| 
      
 565 
     | 
    
         
            +
             
     | 
| 
      
 566 
     | 
    
         
            +
                  unless tstat.to_ThermostatSetpointDualSetpoint.empty?
         
     | 
| 
      
 567 
     | 
    
         
            +
                    tstat = tstat.to_ThermostatSetpointDualSetpoint.get
         
     | 
| 
      
 568 
     | 
    
         
            +
                  else
         
     | 
| 
      
 569 
     | 
    
         
            +
                    tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get
         
     | 
| 
      
 570 
     | 
    
         
            +
                  end
         
     | 
| 
      
 571 
     | 
    
         
            +
             
     | 
| 
      
 572 
     | 
    
         
            +
                  unless tstat.coolingSetpointTemperatureSchedule.empty?
         
     | 
| 
      
 573 
     | 
    
         
            +
                    sched = tstat.coolingSetpointTemperatureSchedule.get
         
     | 
| 
      
 574 
     | 
    
         
            +
             
     | 
| 
      
 575 
     | 
    
         
            +
                    unless sched.to_ScheduleRuleset.empty?
         
     | 
| 
      
 576 
     | 
    
         
            +
                      sched = sched.to_ScheduleRuleset.get
         
     | 
| 
      
 577 
     | 
    
         
            +
                      min = scheduleRulesetMinMax(sched)[:min]
         
     | 
| 
      
 578 
     | 
    
         
            +
             
     | 
| 
      
 579 
     | 
    
         
            +
                      if min
         
     | 
| 
      
 580 
     | 
    
         
            +
                        res[:spt] = min unless res[:spt]
         
     | 
| 
      
 581 
     | 
    
         
            +
                        res[:spt] = min     if res[:spt] > min
         
     | 
| 
      
 582 
     | 
    
         
            +
                      end
         
     | 
| 
      
 583 
     | 
    
         
            +
             
     | 
| 
      
 584 
     | 
    
         
            +
                      dd = sched.summerDesignDaySchedule
         
     | 
| 
      
 585 
     | 
    
         
            +
             
     | 
| 
      
 586 
     | 
    
         
            +
                      unless dd.values.empty?
         
     | 
| 
      
 587 
     | 
    
         
            +
                        res[:spt] = dd.values.min unless res[:spt]
         
     | 
| 
      
 588 
     | 
    
         
            +
                        res[:spt] = dd.values.min     if res[:spt] > dd.values.min
         
     | 
| 
      
 589 
     | 
    
         
            +
                      end
         
     | 
| 
      
 590 
     | 
    
         
            +
                    end
         
     | 
| 
      
 591 
     | 
    
         
            +
             
     | 
| 
      
 592 
     | 
    
         
            +
                    unless sched.to_ScheduleConstant.empty?
         
     | 
| 
      
 593 
     | 
    
         
            +
                      sched = sched.to_ScheduleConstant.get
         
     | 
| 
      
 594 
     | 
    
         
            +
                      min = scheduleConstantMinMax(sched)[:min]
         
     | 
| 
      
 595 
     | 
    
         
            +
             
     | 
| 
      
 596 
     | 
    
         
            +
                      if min
         
     | 
| 
      
 597 
     | 
    
         
            +
                        res[:spt] = min unless res[:spt]
         
     | 
| 
      
 598 
     | 
    
         
            +
                        res[:spt] = min     if res[:spt] > min
         
     | 
| 
      
 599 
     | 
    
         
            +
                      end
         
     | 
| 
      
 600 
     | 
    
         
            +
                    end
         
     | 
| 
      
 601 
     | 
    
         
            +
             
     | 
| 
      
 602 
     | 
    
         
            +
                    unless sched.to_ScheduleCompact.empty?
         
     | 
| 
      
 603 
     | 
    
         
            +
                      sched = sched.to_ScheduleCompact.get
         
     | 
| 
      
 604 
     | 
    
         
            +
                      min = scheduleCompactMinMax(sched)[:min]
         
     | 
| 
      
 605 
     | 
    
         
            +
             
     | 
| 
      
 606 
     | 
    
         
            +
                      if min
         
     | 
| 
      
 607 
     | 
    
         
            +
                        res[:spt] = min unless res[:spt]
         
     | 
| 
      
 608 
     | 
    
         
            +
                        res[:spt] = min     if res[:spt] > min
         
     | 
| 
      
 609 
     | 
    
         
            +
                      end
         
     | 
| 
      
 610 
     | 
    
         
            +
                    end
         
     | 
| 
      
 611 
     | 
    
         
            +
             
     | 
| 
      
 612 
     | 
    
         
            +
                    unless sched.to_ScheduleYear.empty?
         
     | 
| 
      
 613 
     | 
    
         
            +
                      sched = sched.to_ScheduleYear.get
         
     | 
| 
      
 614 
     | 
    
         
            +
             
     | 
| 
      
 615 
     | 
    
         
            +
                      sched.getScheduleWeeks.each do |week|
         
     | 
| 
      
 616 
     | 
    
         
            +
                        next if week.summerDesignDaySchedule.empty?
         
     | 
| 
      
 617 
     | 
    
         
            +
                        dd = week.summerDesignDaySchedule.get
         
     | 
| 
      
 618 
     | 
    
         
            +
                        next unless dd.values.empty?
         
     | 
| 
      
 619 
     | 
    
         
            +
             
     | 
| 
      
 620 
     | 
    
         
            +
                        res[:spt] = dd.values.min unless res[:spt]
         
     | 
| 
      
 621 
     | 
    
         
            +
                        res[:spt] = dd.values.min     if res[:spt] > dd.values.min
         
     | 
| 
      
 622 
     | 
    
         
            +
                      end
         
     | 
| 
      
 623 
     | 
    
         
            +
                    end
         
     | 
| 
      
 624 
     | 
    
         
            +
                  end
         
     | 
| 
      
 625 
     | 
    
         
            +
                end
         
     | 
| 
      
 626 
     | 
    
         
            +
             
     | 
| 
      
 627 
     | 
    
         
            +
                res
         
     | 
| 
      
 628 
     | 
    
         
            +
              end
         
     | 
| 
      
 629 
     | 
    
         
            +
             
     | 
| 
      
 630 
     | 
    
         
            +
              ##
         
     | 
| 
      
 631 
     | 
    
         
            +
              # Validate if model has zones with valid cooling temperature setpoints.
         
     | 
| 
      
 632 
     | 
    
         
            +
              #
         
     | 
| 
      
 633 
     | 
    
         
            +
              # @param model [OpenStudio::Model::Model] a model
         
     | 
| 
      
 634 
     | 
    
         
            +
              #
         
     | 
| 
      
 635 
     | 
    
         
            +
              # @return [Bool] true if valid cooling temperature setpoints
         
     | 
| 
      
 636 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 637 
     | 
    
         
            +
              def coolingTemperatureSetpoints?(model)
         
     | 
| 
      
 638 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 639 
     | 
    
         
            +
                cl = OpenStudio::Model::Model
         
     | 
| 
      
 640 
     | 
    
         
            +
             
     | 
| 
      
 641 
     | 
    
         
            +
                return invalid("model", mth, 1, DBG, false) unless model
         
     | 
| 
      
 642 
     | 
    
         
            +
                return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
         
     | 
| 
      
 643 
     | 
    
         
            +
             
     | 
| 
      
 644 
     | 
    
         
            +
                model.getThermalZones.each do |zone|
         
     | 
| 
      
 645 
     | 
    
         
            +
                  return true if minCoolScheduledSetpoint(zone)[:spt]
         
     | 
| 
      
 646 
     | 
    
         
            +
                end
         
     | 
| 
      
 647 
     | 
    
         
            +
             
     | 
| 
      
 648 
     | 
    
         
            +
                false
         
     | 
| 
      
 649 
     | 
    
         
            +
              end
         
     | 
| 
      
 650 
     | 
    
         
            +
             
     | 
| 
      
 651 
     | 
    
         
            +
              ##
         
     | 
| 
      
 652 
     | 
    
         
            +
              # Validate if model has zones with HVAC air loops.
         
     | 
| 
      
 653 
     | 
    
         
            +
              #
         
     | 
| 
      
 654 
     | 
    
         
            +
              # @param model [OpenStudio::Model::Model] a model
         
     | 
| 
      
 655 
     | 
    
         
            +
              #
         
     | 
| 
      
 656 
     | 
    
         
            +
              # @return [Bool] true if model has one or more HVAC air loops
         
     | 
| 
      
 657 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 658 
     | 
    
         
            +
              def airLoopsHVAC?(model)
         
     | 
| 
      
 659 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 660 
     | 
    
         
            +
                cl = OpenStudio::Model::Model
         
     | 
| 
      
 661 
     | 
    
         
            +
             
     | 
| 
      
 662 
     | 
    
         
            +
                return invalid("model", mth, 1, DBG, false) unless model
         
     | 
| 
      
 663 
     | 
    
         
            +
                return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl)
         
     | 
| 
      
 664 
     | 
    
         
            +
             
     | 
| 
      
 665 
     | 
    
         
            +
                model.getThermalZones.each do |zone|
         
     | 
| 
      
 666 
     | 
    
         
            +
                  next if zone.canBePlenum
         
     | 
| 
      
 667 
     | 
    
         
            +
                  return true unless zone.airLoopHVACs.empty?
         
     | 
| 
      
 668 
     | 
    
         
            +
                  return true if zone.isPlenum
         
     | 
| 
      
 669 
     | 
    
         
            +
                end
         
     | 
| 
      
 670 
     | 
    
         
            +
             
     | 
| 
      
 671 
     | 
    
         
            +
                false
         
     | 
| 
      
 672 
     | 
    
         
            +
              end
         
     | 
| 
      
 673 
     | 
    
         
            +
             
     | 
| 
      
 674 
     | 
    
         
            +
              ##
         
     | 
| 
      
 675 
     | 
    
         
            +
              # Validate whether space should be processed as a plenum.
         
     | 
| 
      
 676 
     | 
    
         
            +
              #
         
     | 
| 
      
 677 
     | 
    
         
            +
              # @param space [OpenStudio::Model::Space] a space
         
     | 
| 
      
 678 
     | 
    
         
            +
              # @param loops [Bool] true if model has airLoopHVAC object(s)
         
     | 
| 
      
 679 
     | 
    
         
            +
              # @param setpoints [Bool] true if model has valid temperature setpoints
         
     | 
| 
      
 680 
     | 
    
         
            +
              #
         
     | 
| 
      
 681 
     | 
    
         
            +
              # @return [Bool] true if should be tagged as plenum
         
     | 
| 
      
 682 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 683 
     | 
    
         
            +
              def plenum?(space, loops, setpoints)
         
     | 
| 
      
 684 
     | 
    
         
            +
                # Largely inspired from NREL's "space_plenum?" procedure:
         
     | 
| 
      
 685 
     | 
    
         
            +
                #
         
     | 
| 
      
 686 
     | 
    
         
            +
                # github.com/NREL/openstudio-standards/blob/
         
     | 
| 
      
 687 
     | 
    
         
            +
                # 58964222d25783e9da4ae292e375fb0d5c902aa5/lib/openstudio-standards/
         
     | 
| 
      
 688 
     | 
    
         
            +
                # standards/Standards.Space.rb#L1384
         
     | 
| 
      
 689 
     | 
    
         
            +
             
     | 
| 
      
 690 
     | 
    
         
            +
                # A space may be tagged as a plenum if:
         
     | 
| 
      
 691 
     | 
    
         
            +
                #
         
     | 
| 
      
 692 
     | 
    
         
            +
                # CASE A: its zone's "isPlenum" == true (SDK method) for a fully-developed
         
     | 
| 
      
 693 
     | 
    
         
            +
                #         OpenStudio model (complete with HVAC air loops);
         
     | 
| 
      
 694 
     | 
    
         
            +
                #
         
     | 
| 
      
 695 
     | 
    
         
            +
                # CASE B: it's excluded from building's total floor area yet linked to a
         
     | 
| 
      
 696 
     | 
    
         
            +
                #         zone holding an "inactive" thermostat (i.e., can't extract
         
     | 
| 
      
 697 
     | 
    
         
            +
                #         valid setpoints);
         
     | 
| 
      
 698 
     | 
    
         
            +
                #
         
     | 
| 
      
 699 
     | 
    
         
            +
                # CASE C: it has a spacetype whose name holds "plenum", or a spacetype with
         
     | 
| 
      
 700 
     | 
    
         
            +
                #         a 'standards spacetype' holding "plenum" (case insensitive); OR
         
     | 
| 
      
 701 
     | 
    
         
            +
                #
         
     | 
| 
      
 702 
     | 
    
         
            +
                # CASE D: its name string holds "plenum" (also case insensitive).
         
     | 
| 
      
 703 
     | 
    
         
            +
             
     | 
| 
      
 704 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 705 
     | 
    
         
            +
                cl  = OpenStudio::Model::Space
         
     | 
| 
      
 706 
     | 
    
         
            +
             
     | 
| 
      
 707 
     | 
    
         
            +
                return invalid("space", mth, 1, DBG, false) unless space.respond_to?(NS)
         
     | 
| 
      
 708 
     | 
    
         
            +
                id = space.nameString
         
     | 
| 
      
 709 
     | 
    
         
            +
                return mismatch(id, space, cl, mth, DBG, false) unless space.is_a?(cl)
         
     | 
| 
      
 710 
     | 
    
         
            +
             
     | 
| 
      
 711 
     | 
    
         
            +
                valid = loops == true || loops == false
         
     | 
| 
      
 712 
     | 
    
         
            +
                return invalid("loops", mth, 2, DBG, false) unless valid
         
     | 
| 
      
 713 
     | 
    
         
            +
             
     | 
| 
      
 714 
     | 
    
         
            +
                valid = setpoints == true || setpoints == false
         
     | 
| 
      
 715 
     | 
    
         
            +
                return invalid("setpoints", mth, 3, DBG, false) unless valid
         
     | 
| 
      
 716 
     | 
    
         
            +
             
     | 
| 
      
 717 
     | 
    
         
            +
                unless space.thermalZone.empty?
         
     | 
| 
      
 718 
     | 
    
         
            +
                  zone = space.thermalZone.get
         
     | 
| 
      
 719 
     | 
    
         
            +
                  return true if zone.isPlenum && loops                             # CASE A
         
     | 
| 
      
 720 
     | 
    
         
            +
             
     | 
| 
      
 721 
     | 
    
         
            +
                  if setpoints
         
     | 
| 
      
 722 
     | 
    
         
            +
                    heating = maxHeatScheduledSetpoint(zone)
         
     | 
| 
      
 723 
     | 
    
         
            +
                    cooling = minCoolScheduledSetpoint(zone)
         
     | 
| 
      
 724 
     | 
    
         
            +
             
     | 
| 
      
 725 
     | 
    
         
            +
                    return false if heating[:spt] || cooling[:spt]    # directly conditioned
         
     | 
| 
      
 726 
     | 
    
         
            +
             
     | 
| 
      
 727 
     | 
    
         
            +
                    unless space.partofTotalFloorArea
         
     | 
| 
      
 728 
     | 
    
         
            +
                      return true if heating[:dual] || cooling[:dual]               # CASE B
         
     | 
| 
      
 729 
     | 
    
         
            +
                    end
         
     | 
| 
      
 730 
     | 
    
         
            +
                  end
         
     | 
| 
      
 731 
     | 
    
         
            +
                end
         
     | 
| 
      
 732 
     | 
    
         
            +
             
     | 
| 
      
 733 
     | 
    
         
            +
                unless space.spaceType.empty?
         
     | 
| 
      
 734 
     | 
    
         
            +
                  type = space.spaceType.get
         
     | 
| 
      
 735 
     | 
    
         
            +
                  return true if type.nameString.downcase.include?("plenum")        # CASE C
         
     | 
| 
      
 736 
     | 
    
         
            +
             
     | 
| 
      
 737 
     | 
    
         
            +
                  unless type.standardsSpaceType.empty?
         
     | 
| 
      
 738 
     | 
    
         
            +
                    type = type.standardsSpaceType.get
         
     | 
| 
      
 739 
     | 
    
         
            +
                    return true if type.downcase.include?("plenum")                 # CASE C
         
     | 
| 
      
 740 
     | 
    
         
            +
                  end
         
     | 
| 
      
 741 
     | 
    
         
            +
                end
         
     | 
| 
      
 742 
     | 
    
         
            +
             
     | 
| 
      
 743 
     | 
    
         
            +
                return true if space.nameString.downcase.include?("plenum")
         
     | 
| 
      
 744 
     | 
    
         
            +
             
     | 
| 
      
 745 
     | 
    
         
            +
                false
         
     | 
| 
      
 746 
     | 
    
         
            +
              end
         
     | 
| 
      
 747 
     | 
    
         
            +
             
     | 
| 
      
 748 
     | 
    
         
            +
              ##
         
     | 
| 
      
 749 
     | 
    
         
            +
              # Generate an HVAC availability schedule.
         
     | 
| 
      
 750 
     | 
    
         
            +
              #
         
     | 
| 
      
 751 
     | 
    
         
            +
              # @param model [OpenStudio::Model::Model] a model
         
     | 
| 
      
 752 
     | 
    
         
            +
              # @param avl [String] seasonal availability choice (optional, default "ON")
         
     | 
| 
      
 753 
     | 
    
         
            +
              #
         
     | 
| 
      
 754 
     | 
    
         
            +
              # @return [OpenStudio::Model::Schedule] HVAC availability sched
         
     | 
| 
      
 755 
     | 
    
         
            +
              # @return [nil] if invalid input
         
     | 
| 
      
 756 
     | 
    
         
            +
              def availabilitySchedule(model, avl = "")
         
     | 
| 
      
 757 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 758 
     | 
    
         
            +
                cl = OpenStudio::Model::Model
         
     | 
| 
      
 759 
     | 
    
         
            +
             
     | 
| 
      
 760 
     | 
    
         
            +
                return invalid("model", mth, 1) unless model
         
     | 
| 
      
 761 
     | 
    
         
            +
                return mismatch("model", model, cl, mth) unless model.is_a?(cl)
         
     | 
| 
      
 762 
     | 
    
         
            +
             
     | 
| 
      
 763 
     | 
    
         
            +
                # Either fetch availability ScheduleTypeLimits object, or create one.
         
     | 
| 
      
 764 
     | 
    
         
            +
                limits = nil
         
     | 
| 
      
 765 
     | 
    
         
            +
             
     | 
| 
      
 766 
     | 
    
         
            +
                model.getScheduleTypeLimitss.each do |l|
         
     | 
| 
      
 767 
     | 
    
         
            +
                  break if limits
         
     | 
| 
      
 768 
     | 
    
         
            +
                  next if l.lowerLimitValue.empty?
         
     | 
| 
      
 769 
     | 
    
         
            +
                  next if l.upperLimitValue.empty?
         
     | 
| 
      
 770 
     | 
    
         
            +
                  next if l.numericType.empty?
         
     | 
| 
      
 771 
     | 
    
         
            +
                  next unless l.lowerLimitValue.get.to_i == 0
         
     | 
| 
      
 772 
     | 
    
         
            +
                  next unless l.upperLimitValue.get.to_i == 1
         
     | 
| 
      
 773 
     | 
    
         
            +
                  next unless l.numericType.get.downcase == "discrete"
         
     | 
| 
      
 774 
     | 
    
         
            +
                  next unless l.unitType.downcase == "availability"
         
     | 
| 
      
 775 
     | 
    
         
            +
                  next unless l.nameString.downcase == "hvac operation scheduletypelimits"
         
     | 
| 
      
 776 
     | 
    
         
            +
                  limits = l
         
     | 
| 
      
 777 
     | 
    
         
            +
                end
         
     | 
| 
      
 778 
     | 
    
         
            +
             
     | 
| 
      
 779 
     | 
    
         
            +
                unless limits
         
     | 
| 
      
 780 
     | 
    
         
            +
                  limits = OpenStudio::Model::ScheduleTypeLimits.new(model)
         
     | 
| 
      
 781 
     | 
    
         
            +
                  limits.setName("HVAC Operation ScheduleTypeLimits")
         
     | 
| 
      
 782 
     | 
    
         
            +
                  limits.setLowerLimitValue(0)
         
     | 
| 
      
 783 
     | 
    
         
            +
                  limits.setUpperLimitValue(1)
         
     | 
| 
      
 784 
     | 
    
         
            +
                  limits.setNumericType("Discrete")
         
     | 
| 
      
 785 
     | 
    
         
            +
                  limits.setUnitType("Availability")
         
     | 
| 
      
 786 
     | 
    
         
            +
                end
         
     | 
| 
      
 787 
     | 
    
         
            +
             
     | 
| 
      
 788 
     | 
    
         
            +
                time = OpenStudio::Time.new(0,24)
         
     | 
| 
      
 789 
     | 
    
         
            +
                secs = time.totalSeconds
         
     | 
| 
      
 790 
     | 
    
         
            +
                on   = OpenStudio::Model::ScheduleDay.new(model, 1)
         
     | 
| 
      
 791 
     | 
    
         
            +
                off  = OpenStudio::Model::ScheduleDay.new(model, 0)
         
     | 
| 
      
 792 
     | 
    
         
            +
             
     | 
| 
      
 793 
     | 
    
         
            +
                # Seasonal availability start/end dates.
         
     | 
| 
      
 794 
     | 
    
         
            +
                year = model.yearDescription
         
     | 
| 
      
 795 
     | 
    
         
            +
                return empty("yearDescription", mth, ERR) if year.empty?
         
     | 
| 
      
 796 
     | 
    
         
            +
                year = year.get
         
     | 
| 
      
 797 
     | 
    
         
            +
                may01 = year.makeDate(OpenStudio::MonthOfYear.new("May"),  1)
         
     | 
| 
      
 798 
     | 
    
         
            +
                oct31 = year.makeDate(OpenStudio::MonthOfYear.new("Oct"), 31)
         
     | 
| 
      
 799 
     | 
    
         
            +
             
     | 
| 
      
 800 
     | 
    
         
            +
                case avl.downcase
         
     | 
| 
      
 801 
     | 
    
         
            +
                when "winter"             # available from November 1 to April 30 (6 months)
         
     | 
| 
      
 802 
     | 
    
         
            +
                  val = 1
         
     | 
| 
      
 803 
     | 
    
         
            +
                  sch = off
         
     | 
| 
      
 804 
     | 
    
         
            +
                  nom = "WINTER Availability SchedRuleset"
         
     | 
| 
      
 805 
     | 
    
         
            +
                  dft = "WINTER Availability dftDaySched"
         
     | 
| 
      
 806 
     | 
    
         
            +
                  tag = "May-Oct WINTER Availability SchedRule"
         
     | 
| 
      
 807 
     | 
    
         
            +
                  day = "May-Oct WINTER SchedRule Day"
         
     | 
| 
      
 808 
     | 
    
         
            +
                when "summer"                # available from May 1 to October 31 (6 months)
         
     | 
| 
      
 809 
     | 
    
         
            +
                  val = 0
         
     | 
| 
      
 810 
     | 
    
         
            +
                  sch = on
         
     | 
| 
      
 811 
     | 
    
         
            +
                  nom = "SUMMER Availability SchedRuleset"
         
     | 
| 
      
 812 
     | 
    
         
            +
                  dft = "SUMMER Availability dftDaySched"
         
     | 
| 
      
 813 
     | 
    
         
            +
                  tag = "May-Oct SUMMER Availability SchedRule"
         
     | 
| 
      
 814 
     | 
    
         
            +
                  day = "May-Oct SUMMER SchedRule Day"
         
     | 
| 
      
 815 
     | 
    
         
            +
                when "off"                                                 # never available
         
     | 
| 
      
 816 
     | 
    
         
            +
                  val = 0
         
     | 
| 
      
 817 
     | 
    
         
            +
                  sch = on
         
     | 
| 
      
 818 
     | 
    
         
            +
                  nom = "OFF Availability SchedRuleset"
         
     | 
| 
      
 819 
     | 
    
         
            +
                  dft = "OFF Availability dftDaySched"
         
     | 
| 
      
 820 
     | 
    
         
            +
                  tag = ""
         
     | 
| 
      
 821 
     | 
    
         
            +
                  day = ""
         
     | 
| 
      
 822 
     | 
    
         
            +
                else                                                      # always available
         
     | 
| 
      
 823 
     | 
    
         
            +
                  val = 1
         
     | 
| 
      
 824 
     | 
    
         
            +
                  sch = on
         
     | 
| 
      
 825 
     | 
    
         
            +
                  nom = "ON Availability SchedRuleset"
         
     | 
| 
      
 826 
     | 
    
         
            +
                  dft = "ON Availability dftDaySched"
         
     | 
| 
      
 827 
     | 
    
         
            +
                  tag = ""
         
     | 
| 
      
 828 
     | 
    
         
            +
                  day = ""
         
     | 
| 
      
 829 
     | 
    
         
            +
                end
         
     | 
| 
      
 830 
     | 
    
         
            +
             
     | 
| 
      
 831 
     | 
    
         
            +
                # Fetch existing schedule.
         
     | 
| 
      
 832 
     | 
    
         
            +
                ok = true
         
     | 
| 
      
 833 
     | 
    
         
            +
                schedule = model.getScheduleByName(nom)
         
     | 
| 
      
 834 
     | 
    
         
            +
             
     | 
| 
      
 835 
     | 
    
         
            +
                unless schedule.empty?
         
     | 
| 
      
 836 
     | 
    
         
            +
                  schedule = schedule.get.to_ScheduleRuleset
         
     | 
| 
      
 837 
     | 
    
         
            +
             
     | 
| 
      
 838 
     | 
    
         
            +
                  unless schedule.empty?
         
     | 
| 
      
 839 
     | 
    
         
            +
                    schedule = schedule.get
         
     | 
| 
      
 840 
     | 
    
         
            +
                    default = schedule.defaultDaySchedule
         
     | 
| 
      
 841 
     | 
    
         
            +
                    ok = ok && default.nameString == dft
         
     | 
| 
      
 842 
     | 
    
         
            +
                    ok = ok && default.times.size == 1
         
     | 
| 
      
 843 
     | 
    
         
            +
                    ok = ok && default.values.size == 1
         
     | 
| 
      
 844 
     | 
    
         
            +
                    ok = ok && default.times.first == time
         
     | 
| 
      
 845 
     | 
    
         
            +
                    ok = ok && default.values.first == val
         
     | 
| 
      
 846 
     | 
    
         
            +
                    rules = schedule.scheduleRules
         
     | 
| 
      
 847 
     | 
    
         
            +
                    ok = ok && (rules.size == 0 || rules.size == 1)
         
     | 
| 
      
 848 
     | 
    
         
            +
             
     | 
| 
      
 849 
     | 
    
         
            +
                    if rules.size == 1
         
     | 
| 
      
 850 
     | 
    
         
            +
                      rule = rules.first
         
     | 
| 
      
 851 
     | 
    
         
            +
                      ok = ok && rule.nameString == tag
         
     | 
| 
      
 852 
     | 
    
         
            +
                      ok = ok && !rule.startDate.empty?
         
     | 
| 
      
 853 
     | 
    
         
            +
                      ok = ok && !rule.endDate.empty?
         
     | 
| 
      
 854 
     | 
    
         
            +
                      ok = ok && rule.startDate.get == may01
         
     | 
| 
      
 855 
     | 
    
         
            +
                      ok = ok && rule.endDate.get == oct31
         
     | 
| 
      
 856 
     | 
    
         
            +
                      ok = ok && rule.applyAllDays
         
     | 
| 
      
 857 
     | 
    
         
            +
             
     | 
| 
      
 858 
     | 
    
         
            +
                      d = rule.daySchedule
         
     | 
| 
      
 859 
     | 
    
         
            +
                      ok = ok && d.nameString == day
         
     | 
| 
      
 860 
     | 
    
         
            +
                      ok = ok && d.times.size == 1
         
     | 
| 
      
 861 
     | 
    
         
            +
                      ok = ok && d.values.size == 1
         
     | 
| 
      
 862 
     | 
    
         
            +
                      ok = ok && d.times.first.totalSeconds == secs
         
     | 
| 
      
 863 
     | 
    
         
            +
                      ok = ok && d.values.first.to_i != val
         
     | 
| 
      
 864 
     | 
    
         
            +
                    end
         
     | 
| 
      
 865 
     | 
    
         
            +
             
     | 
| 
      
 866 
     | 
    
         
            +
                    return schedule if ok
         
     | 
| 
      
 867 
     | 
    
         
            +
                  end
         
     | 
| 
      
 868 
     | 
    
         
            +
                end
         
     | 
| 
      
 869 
     | 
    
         
            +
             
     | 
| 
      
 870 
     | 
    
         
            +
                schedule = OpenStudio::Model::ScheduleRuleset.new(model)
         
     | 
| 
      
 871 
     | 
    
         
            +
                schedule.setName(nom)
         
     | 
| 
      
 872 
     | 
    
         
            +
             
     | 
| 
      
 873 
     | 
    
         
            +
                unless schedule.setScheduleTypeLimits(limits)
         
     | 
| 
      
 874 
     | 
    
         
            +
                  log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})")
         
     | 
| 
      
 875 
     | 
    
         
            +
                  return nil
         
     | 
| 
      
 876 
     | 
    
         
            +
                end
         
     | 
| 
      
 877 
     | 
    
         
            +
             
     | 
| 
      
 878 
     | 
    
         
            +
                unless schedule.defaultDaySchedule.addValue(time, val)
         
     | 
| 
      
 879 
     | 
    
         
            +
                  log(ERR, "'#{nom}': Can't set default day schedule (#{mth})")
         
     | 
| 
      
 880 
     | 
    
         
            +
                  return nil
         
     | 
| 
      
 881 
     | 
    
         
            +
                end
         
     | 
| 
      
 882 
     | 
    
         
            +
             
     | 
| 
      
 883 
     | 
    
         
            +
                schedule.defaultDaySchedule.setName(dft)
         
     | 
| 
      
 884 
     | 
    
         
            +
             
     | 
| 
      
 885 
     | 
    
         
            +
                unless tag.empty?
         
     | 
| 
      
 886 
     | 
    
         
            +
                  rule = OpenStudio::Model::ScheduleRule.new(schedule, sch)
         
     | 
| 
      
 887 
     | 
    
         
            +
                  rule.setName(tag)
         
     | 
| 
      
 888 
     | 
    
         
            +
             
     | 
| 
      
 889 
     | 
    
         
            +
                  unless rule.setStartDate(may01)
         
     | 
| 
      
 890 
     | 
    
         
            +
                    log(ERR, "'#{tag}': Can't set start date (#{mth})")
         
     | 
| 
      
 891 
     | 
    
         
            +
                    return nil
         
     | 
| 
      
 892 
     | 
    
         
            +
                  end
         
     | 
| 
      
 893 
     | 
    
         
            +
             
     | 
| 
      
 894 
     | 
    
         
            +
                  unless rule.setEndDate(oct31)
         
     | 
| 
      
 895 
     | 
    
         
            +
                    log(ERR, "'#{tag}': Can't set end date (#{mth})")
         
     | 
| 
      
 896 
     | 
    
         
            +
                    return nil
         
     | 
| 
      
 897 
     | 
    
         
            +
                  end
         
     | 
| 
      
 898 
     | 
    
         
            +
             
     | 
| 
      
 899 
     | 
    
         
            +
                  unless rule.setApplyAllDays(true)
         
     | 
| 
      
 900 
     | 
    
         
            +
                    log(ERR, "'#{tag}': Can't apply to all days (#{mth})")
         
     | 
| 
      
 901 
     | 
    
         
            +
                    return nil
         
     | 
| 
      
 902 
     | 
    
         
            +
                  end
         
     | 
| 
      
 903 
     | 
    
         
            +
             
     | 
| 
      
 904 
     | 
    
         
            +
                  rule.daySchedule.setName(day)
         
     | 
| 
      
 905 
     | 
    
         
            +
                end
         
     | 
| 
      
 906 
     | 
    
         
            +
             
     | 
| 
      
 907 
     | 
    
         
            +
                schedule
         
     | 
| 
      
 908 
     | 
    
         
            +
              end
         
     | 
| 
      
 909 
     | 
    
         
            +
             
     | 
| 
      
 910 
     | 
    
         
            +
              ##
         
     | 
| 
      
 911 
     | 
    
         
            +
              # Validate if default construction set holds a base ground construction.
         
     | 
| 
      
 912 
     | 
    
         
            +
              #
         
     | 
| 
      
 913 
     | 
    
         
            +
              # @param set [OpenStudio::Model::DefaultConstructionSet] a default set
         
     | 
| 
      
 914 
     | 
    
         
            +
              # @param base [OpensStudio::Model::ConstructionBase] a construction base
         
     | 
| 
      
 915 
     | 
    
         
            +
              # @param ground [Bool] true if ground-facing surface
         
     | 
| 
      
 916 
     | 
    
         
            +
              # @param exterior [Bool] true if exterior-facing surface
         
     | 
| 
      
 917 
     | 
    
         
            +
              # @param type [String] a surface type
         
     | 
| 
      
 918 
     | 
    
         
            +
              #
         
     | 
| 
      
 919 
     | 
    
         
            +
              # @return [Bool] true if default construction set holds construction
         
     | 
| 
      
 920 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 921 
     | 
    
         
            +
              def holdsConstruction?(set, base, ground = false, exterior = false, type = "")
         
     | 
| 
      
 922 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 923 
     | 
    
         
            +
                cl1 = OpenStudio::Model::DefaultConstructionSet
         
     | 
| 
      
 924 
     | 
    
         
            +
                cl2 = OpenStudio::Model::ConstructionBase
         
     | 
| 
      
 925 
     | 
    
         
            +
             
     | 
| 
      
 926 
     | 
    
         
            +
                return invalid("set", mth, 1, DBG, false) unless set.respond_to?(NS)
         
     | 
| 
      
 927 
     | 
    
         
            +
                id = set.nameString
         
     | 
| 
      
 928 
     | 
    
         
            +
                return mismatch(id, set, cl1, mth, DBG, false) unless set.is_a?(cl1)
         
     | 
| 
      
 929 
     | 
    
         
            +
             
     | 
| 
      
 930 
     | 
    
         
            +
                return invalid("base", mth, 2, DBG, false) unless base.respond_to?(NS)
         
     | 
| 
      
 931 
     | 
    
         
            +
                id = base.nameString
         
     | 
| 
      
 932 
     | 
    
         
            +
                return mismatch(id, base, cl2, mth, DBG, false) unless base.is_a?(cl2)
         
     | 
| 
      
 933 
     | 
    
         
            +
             
     | 
| 
      
 934 
     | 
    
         
            +
                valid = ground == true || ground == false
         
     | 
| 
      
 935 
     | 
    
         
            +
                return invalid("ground", mth, 3, DBG, false) unless valid
         
     | 
| 
      
 936 
     | 
    
         
            +
             
     | 
| 
      
 937 
     | 
    
         
            +
                valid = exterior == true || exterior == false
         
     | 
| 
      
 938 
     | 
    
         
            +
                return invalid("exterior", mth, 4, DBG, false) unless valid
         
     | 
| 
      
 939 
     | 
    
         
            +
             
     | 
| 
      
 940 
     | 
    
         
            +
                typ = type.to_s.downcase
         
     | 
| 
      
 941 
     | 
    
         
            +
                valid = typ == "floor" || typ == "wall" || typ == "roofceiling"
         
     | 
| 
      
 942 
     | 
    
         
            +
                return invalid("surface type", mth, 5, DBG, false) unless valid
         
     | 
| 
      
 943 
     | 
    
         
            +
             
     | 
| 
      
 944 
     | 
    
         
            +
                constructions = nil
         
     | 
| 
      
 945 
     | 
    
         
            +
             
     | 
| 
      
 946 
     | 
    
         
            +
                if ground
         
     | 
| 
      
 947 
     | 
    
         
            +
                  unless set.defaultGroundContactSurfaceConstructions.empty?
         
     | 
| 
      
 948 
     | 
    
         
            +
                    constructions = set.defaultGroundContactSurfaceConstructions.get
         
     | 
| 
      
 949 
     | 
    
         
            +
                  end
         
     | 
| 
      
 950 
     | 
    
         
            +
                elsif exterior
         
     | 
| 
      
 951 
     | 
    
         
            +
                  unless set.defaultExteriorSurfaceConstructions.empty?
         
     | 
| 
      
 952 
     | 
    
         
            +
                    constructions = set.defaultExteriorSurfaceConstructions.get
         
     | 
| 
      
 953 
     | 
    
         
            +
                  end
         
     | 
| 
      
 954 
     | 
    
         
            +
                else
         
     | 
| 
      
 955 
     | 
    
         
            +
                  unless set.defaultInteriorSurfaceConstructions.empty?
         
     | 
| 
      
 956 
     | 
    
         
            +
                    constructions = set.defaultInteriorSurfaceConstructions.get
         
     | 
| 
      
 957 
     | 
    
         
            +
                  end
         
     | 
| 
      
 958 
     | 
    
         
            +
                end
         
     | 
| 
      
 959 
     | 
    
         
            +
             
     | 
| 
      
 960 
     | 
    
         
            +
                return false unless constructions
         
     | 
| 
      
 961 
     | 
    
         
            +
             
     | 
| 
      
 962 
     | 
    
         
            +
                case typ
         
     | 
| 
      
 963 
     | 
    
         
            +
                when "roofceiling"
         
     | 
| 
      
 964 
     | 
    
         
            +
                  unless constructions.roofCeilingConstruction.empty?
         
     | 
| 
      
 965 
     | 
    
         
            +
                    construction = constructions.roofCeilingConstruction.get
         
     | 
| 
      
 966 
     | 
    
         
            +
                    return true if construction == base
         
     | 
| 
      
 967 
     | 
    
         
            +
                  end
         
     | 
| 
      
 968 
     | 
    
         
            +
                when "floor"
         
     | 
| 
      
 969 
     | 
    
         
            +
                  unless constructions.floorConstruction.empty?
         
     | 
| 
      
 970 
     | 
    
         
            +
                    construction = constructions.floorConstruction.get
         
     | 
| 
      
 971 
     | 
    
         
            +
                    return true if construction == base
         
     | 
| 
      
 972 
     | 
    
         
            +
                  end
         
     | 
| 
      
 973 
     | 
    
         
            +
                else
         
     | 
| 
      
 974 
     | 
    
         
            +
                  unless constructions.wallConstruction.empty?
         
     | 
| 
      
 975 
     | 
    
         
            +
                    construction = constructions.wallConstruction.get
         
     | 
| 
      
 976 
     | 
    
         
            +
                    return true if construction == base
         
     | 
| 
      
 977 
     | 
    
         
            +
                  end
         
     | 
| 
      
 978 
     | 
    
         
            +
                end
         
     | 
| 
      
 979 
     | 
    
         
            +
             
     | 
| 
      
 980 
     | 
    
         
            +
                false
         
     | 
| 
      
 981 
     | 
    
         
            +
              end
         
     | 
| 
      
 982 
     | 
    
         
            +
             
     | 
| 
      
 983 
     | 
    
         
            +
              ##
         
     | 
| 
      
 984 
     | 
    
         
            +
              # Return a surface's default construction set.
         
     | 
| 
      
 985 
     | 
    
         
            +
              #
         
     | 
| 
      
 986 
     | 
    
         
            +
              # @param model [OpenStudio::Model::Model] a model
         
     | 
| 
      
 987 
     | 
    
         
            +
              # @param s [OpenStudio::Model::Surface] a surface
         
     | 
| 
      
 988 
     | 
    
         
            +
              #
         
     | 
| 
      
 989 
     | 
    
         
            +
              # @return [OpenStudio::Model::DefaultConstructionSet] default set
         
     | 
| 
      
 990 
     | 
    
         
            +
              # @return [nil] if invalid input
         
     | 
| 
      
 991 
     | 
    
         
            +
              def defaultConstructionSet(model, s)
         
     | 
| 
      
 992 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 993 
     | 
    
         
            +
                cl1 = OpenStudio::Model::Model
         
     | 
| 
      
 994 
     | 
    
         
            +
                cl2 = OpenStudio::Model::Surface
         
     | 
| 
      
 995 
     | 
    
         
            +
             
     | 
| 
      
 996 
     | 
    
         
            +
                return invalid("model", mth, 1) unless model
         
     | 
| 
      
 997 
     | 
    
         
            +
                return mismatch("model", model, cl1, mth) unless model.is_a?(cl1)
         
     | 
| 
      
 998 
     | 
    
         
            +
             
     | 
| 
      
 999 
     | 
    
         
            +
                return invalid("s", mth, 2) unless s.respond_to?(NS)
         
     | 
| 
      
 1000 
     | 
    
         
            +
                id = s.nameString
         
     | 
| 
      
 1001 
     | 
    
         
            +
                return mismatch(id, s, cl2, mth) unless s.is_a?(cl2)
         
     | 
| 
      
 1002 
     | 
    
         
            +
             
     | 
| 
      
 1003 
     | 
    
         
            +
                unless s.isConstructionDefaulted
         
     | 
| 
      
 1004 
     | 
    
         
            +
                  log(ERR, "'#{id}' construction not defaulted (#{mth})")
         
     | 
| 
      
 1005 
     | 
    
         
            +
                  return nil
         
     | 
| 
      
 1006 
     | 
    
         
            +
                end
         
     | 
| 
      
 1007 
     | 
    
         
            +
             
     | 
| 
      
 1008 
     | 
    
         
            +
                return empty("'#{id}' construction", mth, ERR) if s.construction.empty?
         
     | 
| 
      
 1009 
     | 
    
         
            +
                base = s.construction.get
         
     | 
| 
      
 1010 
     | 
    
         
            +
                return empty("'#{id}' space", mth, ERR) if s.space.empty?
         
     | 
| 
      
 1011 
     | 
    
         
            +
                space = s.space.get
         
     | 
| 
      
 1012 
     | 
    
         
            +
                type = s.surfaceType
         
     | 
| 
      
 1013 
     | 
    
         
            +
             
     | 
| 
      
 1014 
     | 
    
         
            +
                ground = false
         
     | 
| 
      
 1015 
     | 
    
         
            +
                exterior = false
         
     | 
| 
      
 1016 
     | 
    
         
            +
             
     | 
| 
      
 1017 
     | 
    
         
            +
                if s.isGroundSurface
         
     | 
| 
      
 1018 
     | 
    
         
            +
                  ground = true
         
     | 
| 
      
 1019 
     | 
    
         
            +
                elsif s.outsideBoundaryCondition.downcase == "outdoors"
         
     | 
| 
      
 1020 
     | 
    
         
            +
                  exterior = true
         
     | 
| 
      
 1021 
     | 
    
         
            +
                end
         
     | 
| 
      
 1022 
     | 
    
         
            +
             
     | 
| 
      
 1023 
     | 
    
         
            +
                unless space.defaultConstructionSet.empty?
         
     | 
| 
      
 1024 
     | 
    
         
            +
                  set = space.defaultConstructionSet.get
         
     | 
| 
      
 1025 
     | 
    
         
            +
                  return set if holdsConstruction?(set, base, ground, exterior, type)
         
     | 
| 
      
 1026 
     | 
    
         
            +
                end
         
     | 
| 
      
 1027 
     | 
    
         
            +
             
     | 
| 
      
 1028 
     | 
    
         
            +
                unless space.spaceType.empty?
         
     | 
| 
      
 1029 
     | 
    
         
            +
                  spacetype = space.spaceType.get
         
     | 
| 
      
 1030 
     | 
    
         
            +
             
     | 
| 
      
 1031 
     | 
    
         
            +
                  unless spacetype.defaultConstructionSet.empty?
         
     | 
| 
      
 1032 
     | 
    
         
            +
                    set = spacetype.defaultConstructionSet.get
         
     | 
| 
      
 1033 
     | 
    
         
            +
                    return set if holdsConstruction?(set, base, ground, exterior, type)
         
     | 
| 
      
 1034 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1035 
     | 
    
         
            +
                end
         
     | 
| 
      
 1036 
     | 
    
         
            +
             
     | 
| 
      
 1037 
     | 
    
         
            +
                unless space.buildingStory.empty?
         
     | 
| 
      
 1038 
     | 
    
         
            +
                  story = space.buildingStory.get
         
     | 
| 
      
 1039 
     | 
    
         
            +
             
     | 
| 
      
 1040 
     | 
    
         
            +
                  unless story.defaultConstructionSet.empty?
         
     | 
| 
      
 1041 
     | 
    
         
            +
                    set = story.defaultConstructionSet.get
         
     | 
| 
      
 1042 
     | 
    
         
            +
                    return set if holdsConstruction?(set, base, ground, exterior, type)
         
     | 
| 
      
 1043 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1044 
     | 
    
         
            +
                end
         
     | 
| 
      
 1045 
     | 
    
         
            +
             
     | 
| 
      
 1046 
     | 
    
         
            +
                building = model.getBuilding
         
     | 
| 
      
 1047 
     | 
    
         
            +
             
     | 
| 
      
 1048 
     | 
    
         
            +
                unless building.defaultConstructionSet.empty?
         
     | 
| 
      
 1049 
     | 
    
         
            +
                  set = building.defaultConstructionSet.get
         
     | 
| 
      
 1050 
     | 
    
         
            +
                  return set if holdsConstruction?(set, base, ground, exterior, type)
         
     | 
| 
      
 1051 
     | 
    
         
            +
                end
         
     | 
| 
      
 1052 
     | 
    
         
            +
             
     | 
| 
      
 1053 
     | 
    
         
            +
                nil
         
     | 
| 
      
 1054 
     | 
    
         
            +
              end
         
     | 
| 
      
 1055 
     | 
    
         
            +
             
     | 
| 
      
 1056 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1057 
     | 
    
         
            +
              # Validate if every material in a layered construction is standard & opaque.
         
     | 
| 
      
 1058 
     | 
    
         
            +
              #
         
     | 
| 
      
 1059 
     | 
    
         
            +
              # @param lc [OpenStudio::LayeredConstruction] a layered construction
         
     | 
| 
      
 1060 
     | 
    
         
            +
              #
         
     | 
| 
      
 1061 
     | 
    
         
            +
              # @return [Bool] true if all layers are valid
         
     | 
| 
      
 1062 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 1063 
     | 
    
         
            +
              def standardOpaqueLayers?(lc)
         
     | 
| 
      
 1064 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1065 
     | 
    
         
            +
                cl  = OpenStudio::Model::LayeredConstruction
         
     | 
| 
      
 1066 
     | 
    
         
            +
             
     | 
| 
      
 1067 
     | 
    
         
            +
                return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS)
         
     | 
| 
      
 1068 
     | 
    
         
            +
                return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl)
         
     | 
| 
      
 1069 
     | 
    
         
            +
             
     | 
| 
      
 1070 
     | 
    
         
            +
                lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? }
         
     | 
| 
      
 1071 
     | 
    
         
            +
                true
         
     | 
| 
      
 1072 
     | 
    
         
            +
              end
         
     | 
| 
      
 1073 
     | 
    
         
            +
             
     | 
| 
      
 1074 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1075 
     | 
    
         
            +
              # Total (standard opaque) layered construction thickness (in m).
         
     | 
| 
      
 1076 
     | 
    
         
            +
              #
         
     | 
| 
      
 1077 
     | 
    
         
            +
              # @param lc [OpenStudio::LayeredConstruction] a layered construction
         
     | 
| 
      
 1078 
     | 
    
         
            +
              #
         
     | 
| 
      
 1079 
     | 
    
         
            +
              # @return [Double] total layered construction thickness
         
     | 
| 
      
 1080 
     | 
    
         
            +
              # @return [Double] 0 if invalid input
         
     | 
| 
      
 1081 
     | 
    
         
            +
              def thickness(lc)
         
     | 
| 
      
 1082 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1083 
     | 
    
         
            +
                cl  = OpenStudio::Model::LayeredConstruction
         
     | 
| 
      
 1084 
     | 
    
         
            +
             
     | 
| 
      
 1085 
     | 
    
         
            +
                return invalid("lc", mth, 1, DBG, 0) unless lc.respond_to?(NS)
         
     | 
| 
      
 1086 
     | 
    
         
            +
                id = lc.nameString
         
     | 
| 
      
 1087 
     | 
    
         
            +
                return mismatch(id, lc, cl, mth, DBG, 0) unless lc.is_a?(cl)
         
     | 
| 
      
 1088 
     | 
    
         
            +
             
     | 
| 
      
 1089 
     | 
    
         
            +
                unless standardOpaqueLayers?(lc)
         
     | 
| 
      
 1090 
     | 
    
         
            +
                  log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})")
         
     | 
| 
      
 1091 
     | 
    
         
            +
                  return 0
         
     | 
| 
      
 1092 
     | 
    
         
            +
                end
         
     | 
| 
      
 1093 
     | 
    
         
            +
             
     | 
| 
      
 1094 
     | 
    
         
            +
                thickness = 0.0
         
     | 
| 
      
 1095 
     | 
    
         
            +
                lc.layers.each { |m| thickness += m.thickness }
         
     | 
| 
      
 1096 
     | 
    
         
            +
                thickness
         
     | 
| 
      
 1097 
     | 
    
         
            +
              end
         
     | 
| 
      
 1098 
     | 
    
         
            +
             
     | 
| 
      
 1099 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1100 
     | 
    
         
            +
              # Return total air film resistance for fenestration.
         
     | 
| 
      
 1101 
     | 
    
         
            +
              #
         
     | 
| 
      
 1102 
     | 
    
         
            +
              # @param usi [Float] a fenestrated construction's U-factor (W/m2•K)
         
     | 
| 
      
 1103 
     | 
    
         
            +
              #
         
     | 
| 
      
 1104 
     | 
    
         
            +
              # @return [Float] total air film resistance in m2•K/W (0.1216 if errors)
         
     | 
| 
      
 1105 
     | 
    
         
            +
              def glazingAirFilmRSi(usi = 5.85)
         
     | 
| 
      
 1106 
     | 
    
         
            +
                # The sum of thermal resistances of calculated exterior and interior film
         
     | 
| 
      
 1107 
     | 
    
         
            +
                # coefficients under standard winter conditions are taken from:
         
     | 
| 
      
 1108 
     | 
    
         
            +
                #
         
     | 
| 
      
 1109 
     | 
    
         
            +
                #   https://bigladdersoftware.com/epx/docs/9-6/engineering-reference/
         
     | 
| 
      
 1110 
     | 
    
         
            +
                #   window-calculation-module.html#simple-window-model
         
     | 
| 
      
 1111 
     | 
    
         
            +
                #
         
     | 
| 
      
 1112 
     | 
    
         
            +
                # These remain acceptable approximations for flat windows, yet likely
         
     | 
| 
      
 1113 
     | 
    
         
            +
                # unsuitable for subsurfaces with curved or projecting shapes like domed
         
     | 
| 
      
 1114 
     | 
    
         
            +
                # skylights. The solution here is considered an adequate fix for reporting,
         
     | 
| 
      
 1115 
     | 
    
         
            +
                # awaiting eventual OpenStudio (and EnergyPlus) upgrades to report NFRC 100
         
     | 
| 
      
 1116 
     | 
    
         
            +
                # (or ISO) air film resistances under standard winter conditions.
         
     | 
| 
      
 1117 
     | 
    
         
            +
                #
         
     | 
| 
      
 1118 
     | 
    
         
            +
                # For U-factors above 8.0 W/m2•K (or invalid input), the function returns
         
     | 
| 
      
 1119 
     | 
    
         
            +
                # 0.1216 m2•K/W, which corresponds to a construction with a single glass
         
     | 
| 
      
 1120 
     | 
    
         
            +
                # layer thickness of 2mm & k = ~0.6 W/m.K.
         
     | 
| 
      
 1121 
     | 
    
         
            +
                #
         
     | 
| 
      
 1122 
     | 
    
         
            +
                # The EnergyPlus Engineering calculations were designed for vertical windows
         
     | 
| 
      
 1123 
     | 
    
         
            +
                # - not horizontal, slanted or domed surfaces - use with caution.
         
     | 
| 
      
 1124 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1125 
     | 
    
         
            +
                cl = Numeric
         
     | 
| 
      
 1126 
     | 
    
         
            +
             
     | 
| 
      
 1127 
     | 
    
         
            +
                return invalid("usi", mth, 1, DBG, 0.1216) unless usi
         
     | 
| 
      
 1128 
     | 
    
         
            +
                return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl)
         
     | 
| 
      
 1129 
     | 
    
         
            +
                return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0
         
     | 
| 
      
 1130 
     | 
    
         
            +
             
     | 
| 
      
 1131 
     | 
    
         
            +
                rsi = 1 / (0.025342 * usi + 29.163853)   # exterior film, next interior film
         
     | 
| 
      
 1132 
     | 
    
         
            +
             
     | 
| 
      
 1133 
     | 
    
         
            +
                return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85
         
     | 
| 
      
 1134 
     | 
    
         
            +
                return rsi + 1 / (1.788041 * usi - 2.886625)
         
     | 
| 
      
 1135 
     | 
    
         
            +
              end
         
     | 
| 
      
 1136 
     | 
    
         
            +
             
     | 
| 
      
 1137 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1138 
     | 
    
         
            +
              # Return a construction's 'standard calc' thermal resistance (with air films).
         
     | 
| 
      
 1139 
     | 
    
         
            +
              #
         
     | 
| 
      
 1140 
     | 
    
         
            +
              # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
         
     | 
| 
      
 1141 
     | 
    
         
            +
              # @param film [Float] thermal resistance of surface air films (m2•K/W)
         
     | 
| 
      
 1142 
     | 
    
         
            +
              # @param t [Float] gas temperature (°C) (optional)
         
     | 
| 
      
 1143 
     | 
    
         
            +
              #
         
     | 
| 
      
 1144 
     | 
    
         
            +
              # @return [Float] calculated RSi at standard conditions (0 if error)
         
     | 
| 
      
 1145 
     | 
    
         
            +
              def rsi(lc, film, t = 0.0)
         
     | 
| 
      
 1146 
     | 
    
         
            +
                # This is adapted from BTAP's Material Module's "get_conductance" (P. Lopez)
         
     | 
| 
      
 1147 
     | 
    
         
            +
                #
         
     | 
| 
      
 1148 
     | 
    
         
            +
                #   https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/
         
     | 
| 
      
 1149 
     | 
    
         
            +
                #   c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/
         
     | 
| 
      
 1150 
     | 
    
         
            +
                #   btap_equest_converter/envelope.rb#L122
         
     | 
| 
      
 1151 
     | 
    
         
            +
             
     | 
| 
      
 1152 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1153 
     | 
    
         
            +
                cl1 = OpenStudio::Model::LayeredConstruction
         
     | 
| 
      
 1154 
     | 
    
         
            +
                cl2 = Numeric
         
     | 
| 
      
 1155 
     | 
    
         
            +
             
     | 
| 
      
 1156 
     | 
    
         
            +
                return invalid("lc", mth, 1, DBG, 0) unless lc.respond_to?(NS)
         
     | 
| 
      
 1157 
     | 
    
         
            +
                id = lc.nameString
         
     | 
| 
      
 1158 
     | 
    
         
            +
                return mismatch(id, lc, cl1, mth, DBG, 0) unless lc.is_a?(cl1)
         
     | 
| 
      
 1159 
     | 
    
         
            +
             
     | 
| 
      
 1160 
     | 
    
         
            +
                return invalid("film", mth, 2, DBG, 0) unless film
         
     | 
| 
      
 1161 
     | 
    
         
            +
                return invalid("temperature", mth, 3, DBG, 0) unless t
         
     | 
| 
      
 1162 
     | 
    
         
            +
             
     | 
| 
      
 1163 
     | 
    
         
            +
                return mismatch("film", film, cl2, mth, DBG, 0) unless film.is_a?(cl2)
         
     | 
| 
      
 1164 
     | 
    
         
            +
                return mismatch("temperature", t, cl2, mth, DBG, 0) unless t.is_a?(cl2)
         
     | 
| 
      
 1165 
     | 
    
         
            +
             
     | 
| 
      
 1166 
     | 
    
         
            +
                tt  = t + 273.0                                                    # °C to K
         
     | 
| 
      
 1167 
     | 
    
         
            +
                return negative("temp K", mth, DBG, 0) if tt < 0
         
     | 
| 
      
 1168 
     | 
    
         
            +
                return negative("film", mth, DBG, 0) if film < 0
         
     | 
| 
      
 1169 
     | 
    
         
            +
             
     | 
| 
      
 1170 
     | 
    
         
            +
                rsi = film
         
     | 
| 
      
 1171 
     | 
    
         
            +
             
     | 
| 
      
 1172 
     | 
    
         
            +
                lc.layers.each do |m|
         
     | 
| 
      
 1173 
     | 
    
         
            +
                  # Fenestration materials first (ignoring shades, screens, etc.)
         
     | 
| 
      
 1174 
     | 
    
         
            +
                  unless m.to_SimpleGlazing.empty?
         
     | 
| 
      
 1175 
     | 
    
         
            +
                    return 1 / m.to_SimpleGlazing.get.uFactor              # no need to loop
         
     | 
| 
      
 1176 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1177 
     | 
    
         
            +
                  unless m.to_StandardGlazing.empty?
         
     | 
| 
      
 1178 
     | 
    
         
            +
                    rsi += m.to_StandardGlazing.get.thermalResistance
         
     | 
| 
      
 1179 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1180 
     | 
    
         
            +
                  unless m.to_RefractionExtinctionGlazing.empty?
         
     | 
| 
      
 1181 
     | 
    
         
            +
                    rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance
         
     | 
| 
      
 1182 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1183 
     | 
    
         
            +
                  unless m.to_Gas.empty?
         
     | 
| 
      
 1184 
     | 
    
         
            +
                    rsi += m.to_Gas.get.getThermalResistance(tt)
         
     | 
| 
      
 1185 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1186 
     | 
    
         
            +
                  unless m.to_GasMixture.empty?
         
     | 
| 
      
 1187 
     | 
    
         
            +
                    rsi += m.to_GasMixture.get.getThermalResistance(tt)
         
     | 
| 
      
 1188 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1189 
     | 
    
         
            +
             
     | 
| 
      
 1190 
     | 
    
         
            +
                  # Opaque materials next.
         
     | 
| 
      
 1191 
     | 
    
         
            +
                  unless m.to_StandardOpaqueMaterial.empty?
         
     | 
| 
      
 1192 
     | 
    
         
            +
                    rsi += m.to_StandardOpaqueMaterial.get.thermalResistance
         
     | 
| 
      
 1193 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1194 
     | 
    
         
            +
                  unless m.to_MasslessOpaqueMaterial.empty?
         
     | 
| 
      
 1195 
     | 
    
         
            +
                    rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance
         
     | 
| 
      
 1196 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1197 
     | 
    
         
            +
                  unless m.to_RoofVegetation.empty?
         
     | 
| 
      
 1198 
     | 
    
         
            +
                    rsi += m.to_RoofVegetation.get.thermalResistance
         
     | 
| 
      
 1199 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1200 
     | 
    
         
            +
                  unless m.to_AirGap.empty?
         
     | 
| 
      
 1201 
     | 
    
         
            +
                    rsi += m.to_AirGap.get.thermalResistance
         
     | 
| 
      
 1202 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1203 
     | 
    
         
            +
                end
         
     | 
| 
      
 1204 
     | 
    
         
            +
             
     | 
| 
      
 1205 
     | 
    
         
            +
                rsi
         
     | 
| 
      
 1206 
     | 
    
         
            +
              end
         
     | 
| 
      
 1207 
     | 
    
         
            +
             
     | 
| 
      
 1208 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1209 
     | 
    
         
            +
              # Identify a layered construction's (opaque) insulating layer. The method
         
     | 
| 
      
 1210 
     | 
    
         
            +
              # returns a 3-keyed hash ... :index (insulating layer index within layered
         
     | 
| 
      
 1211 
     | 
    
         
            +
              # construction), :type (standard: or massless: material type), and
         
     | 
| 
      
 1212 
     | 
    
         
            +
              # :r (material thermal resistance in m2•K/W).
         
     | 
| 
      
 1213 
     | 
    
         
            +
              #
         
     | 
| 
      
 1214 
     | 
    
         
            +
              # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction
         
     | 
| 
      
 1215 
     | 
    
         
            +
              #
         
     | 
| 
      
 1216 
     | 
    
         
            +
              # @return [Hash] index: (Integer), type: (:standard or :massless), r: (Float)
         
     | 
| 
      
 1217 
     | 
    
         
            +
              # @return [Hash] index: nil, type: nil, r: 0 (if invalid input)
         
     | 
| 
      
 1218 
     | 
    
         
            +
              def insulatingLayer(lc)
         
     | 
| 
      
 1219 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1220 
     | 
    
         
            +
                cl  = OpenStudio::Model::LayeredConstruction
         
     | 
| 
      
 1221 
     | 
    
         
            +
                res = { index: nil, type: nil, r: 0.0 }
         
     | 
| 
      
 1222 
     | 
    
         
            +
                i   = 0                                                           # iterator
         
     | 
| 
      
 1223 
     | 
    
         
            +
             
     | 
| 
      
 1224 
     | 
    
         
            +
                return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS)
         
     | 
| 
      
 1225 
     | 
    
         
            +
                id = lc.nameString
         
     | 
| 
      
 1226 
     | 
    
         
            +
                return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl)
         
     | 
| 
      
 1227 
     | 
    
         
            +
             
     | 
| 
      
 1228 
     | 
    
         
            +
                lc.layers.each do |m|
         
     | 
| 
      
 1229 
     | 
    
         
            +
             
     | 
| 
      
 1230 
     | 
    
         
            +
                  unless m.to_MasslessOpaqueMaterial.empty?
         
     | 
| 
      
 1231 
     | 
    
         
            +
                    m             = m.to_MasslessOpaqueMaterial.get
         
     | 
| 
      
 1232 
     | 
    
         
            +
             
     | 
| 
      
 1233 
     | 
    
         
            +
                    if m.thermalResistance < 0.001 || m.thermalResistance < res[:r]
         
     | 
| 
      
 1234 
     | 
    
         
            +
                      i += 1
         
     | 
| 
      
 1235 
     | 
    
         
            +
                      next
         
     | 
| 
      
 1236 
     | 
    
         
            +
                    else
         
     | 
| 
      
 1237 
     | 
    
         
            +
                      res[:r]     = m.thermalResistance
         
     | 
| 
      
 1238 
     | 
    
         
            +
                      res[:index] = i
         
     | 
| 
      
 1239 
     | 
    
         
            +
                      res[:type]  = :massless
         
     | 
| 
      
 1240 
     | 
    
         
            +
                    end
         
     | 
| 
      
 1241 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1242 
     | 
    
         
            +
             
     | 
| 
      
 1243 
     | 
    
         
            +
                  unless m.to_StandardOpaqueMaterial.empty?
         
     | 
| 
      
 1244 
     | 
    
         
            +
                    m             = m.to_StandardOpaqueMaterial.get
         
     | 
| 
      
 1245 
     | 
    
         
            +
                    k             = m.thermalConductivity
         
     | 
| 
      
 1246 
     | 
    
         
            +
                    d             = m.thickness
         
     | 
| 
      
 1247 
     | 
    
         
            +
             
     | 
| 
      
 1248 
     | 
    
         
            +
                    if d < 0.003 || k > 3.0 || d / k < res[:r]
         
     | 
| 
      
 1249 
     | 
    
         
            +
                      i += 1
         
     | 
| 
      
 1250 
     | 
    
         
            +
                      next
         
     | 
| 
      
 1251 
     | 
    
         
            +
                    else
         
     | 
| 
      
 1252 
     | 
    
         
            +
                      res[:r]     = d / k
         
     | 
| 
      
 1253 
     | 
    
         
            +
                      res[:index] = i
         
     | 
| 
      
 1254 
     | 
    
         
            +
                      res[:type]  = :standard
         
     | 
| 
      
 1255 
     | 
    
         
            +
                    end
         
     | 
| 
      
 1256 
     | 
    
         
            +
                  end
         
     | 
| 
      
 1257 
     | 
    
         
            +
             
     | 
| 
      
 1258 
     | 
    
         
            +
                  i += 1
         
     | 
| 
      
 1259 
     | 
    
         
            +
                end
         
     | 
| 
      
 1260 
     | 
    
         
            +
             
     | 
| 
      
 1261 
     | 
    
         
            +
                res
         
     | 
| 
      
 1262 
     | 
    
         
            +
              end
         
     | 
| 
      
 1263 
     | 
    
         
            +
             
     | 
| 
      
 1264 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1265 
     | 
    
         
            +
              # Return OpenStudio site/space transformation & rotation angle [0,2PI) rads.
         
     | 
| 
      
 1266 
     | 
    
         
            +
              #
         
     | 
| 
      
 1267 
     | 
    
         
            +
              # @param model [OpenStudio::Model::Model] a model
         
     | 
| 
      
 1268 
     | 
    
         
            +
              # @param group [OpenStudio::Model::PlanarSurfaceGroup] a group
         
     | 
| 
      
 1269 
     | 
    
         
            +
              #
         
     | 
| 
      
 1270 
     | 
    
         
            +
              # @return [Hash] t: (OpenStudio::Transformation), r: Float
         
     | 
| 
      
 1271 
     | 
    
         
            +
              # @return [Hash] t: nil, r: nil (if invalid input)
         
     | 
| 
      
 1272 
     | 
    
         
            +
              def transforms(model, group)
         
     | 
| 
      
 1273 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1274 
     | 
    
         
            +
                cl1 = OpenStudio::Model::Model
         
     | 
| 
      
 1275 
     | 
    
         
            +
                cl2 = OpenStudio::Model::PlanarSurfaceGroup
         
     | 
| 
      
 1276 
     | 
    
         
            +
                res = { t: nil, r: nil }
         
     | 
| 
      
 1277 
     | 
    
         
            +
             
     | 
| 
      
 1278 
     | 
    
         
            +
                return invalid("model", mth, 1, DBG, res) unless model
         
     | 
| 
      
 1279 
     | 
    
         
            +
                return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1)
         
     | 
| 
      
 1280 
     | 
    
         
            +
             
     | 
| 
      
 1281 
     | 
    
         
            +
                return invalid("group", mth, 2, DBG, res) unless group.respond_to?(NS)
         
     | 
| 
      
 1282 
     | 
    
         
            +
                id = group.nameString
         
     | 
| 
      
 1283 
     | 
    
         
            +
                return mismatch(id, group, cl2, mth, DBG, res) unless group.is_a?(cl2)
         
     | 
| 
      
 1284 
     | 
    
         
            +
             
     | 
| 
      
 1285 
     | 
    
         
            +
                res[:t] = group.siteTransformation
         
     | 
| 
      
 1286 
     | 
    
         
            +
                res[:r] = group.directionofRelativeNorth + model.getBuilding.northAxis
         
     | 
| 
      
 1287 
     | 
    
         
            +
             
     | 
| 
      
 1288 
     | 
    
         
            +
                res
         
     | 
| 
      
 1289 
     | 
    
         
            +
              end
         
     | 
| 
      
 1290 
     | 
    
         
            +
             
     | 
| 
      
 1291 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1292 
     | 
    
         
            +
              # Flatten OpenStudio 3D points vs Z-axis (Z=0).
         
     | 
| 
      
 1293 
     | 
    
         
            +
              #
         
     | 
| 
      
 1294 
     | 
    
         
            +
              # @param pts [Array] an OpenStudio Point3D array/vector
         
     | 
| 
      
 1295 
     | 
    
         
            +
              #
         
     | 
| 
      
 1296 
     | 
    
         
            +
              # @return [Array] flattened OpenStudio 3D points
         
     | 
| 
      
 1297 
     | 
    
         
            +
              def flatZ(pts)
         
     | 
| 
      
 1298 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1299 
     | 
    
         
            +
                cl1 = OpenStudio::Point3dVector
         
     | 
| 
      
 1300 
     | 
    
         
            +
                cl2 = OpenStudio::Point3d
         
     | 
| 
      
 1301 
     | 
    
         
            +
                v = OpenStudio::Point3dVector.new
         
     | 
| 
      
 1302 
     | 
    
         
            +
             
     | 
| 
      
 1303 
     | 
    
         
            +
                return invalid("points", mth, 1, DBG, v) unless pts
         
     | 
| 
      
 1304 
     | 
    
         
            +
                valid = pts.is_a?(cl1) || pts.is_a?(Array)
         
     | 
| 
      
 1305 
     | 
    
         
            +
                return mismatch("points", pts, cl1, mth, DBG, v) unless valid
         
     | 
| 
      
 1306 
     | 
    
         
            +
             
     | 
| 
      
 1307 
     | 
    
         
            +
                pts.each { |pt| mismatch("pt", pt, cl2, mth, ERR, v) unless pt.is_a?(cl2) }
         
     | 
| 
      
 1308 
     | 
    
         
            +
                pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, 0) }
         
     | 
| 
      
 1309 
     | 
    
         
            +
             
     | 
| 
      
 1310 
     | 
    
         
            +
                v
         
     | 
| 
      
 1311 
     | 
    
         
            +
              end
         
     | 
| 
      
 1312 
     | 
    
         
            +
             
     | 
| 
      
 1313 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1314 
     | 
    
         
            +
              # Validate whether 1st OpenStudio convex polygon fits in 2nd convex polygon.
         
     | 
| 
      
 1315 
     | 
    
         
            +
              #
         
     | 
| 
      
 1316 
     | 
    
         
            +
              # @param p1 [OpenStudio::Point3dVector] or Point3D array of polygon #1
         
     | 
| 
      
 1317 
     | 
    
         
            +
              # @param p2 [OpenStudio::Point3dVector] or Point3D array of polygon #2
         
     | 
| 
      
 1318 
     | 
    
         
            +
              # @param id1 [String] polygon #1 identifier (optional)
         
     | 
| 
      
 1319 
     | 
    
         
            +
              # @param id2 [String] polygon #2 identifier (optional)
         
     | 
| 
      
 1320 
     | 
    
         
            +
              #
         
     | 
| 
      
 1321 
     | 
    
         
            +
              # @return [Bool] true if 1st polygon fits entirely within the 2nd polygon
         
     | 
| 
      
 1322 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 1323 
     | 
    
         
            +
              def fits?(p1, p2, id1 = "", id2 = "")
         
     | 
| 
      
 1324 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1325 
     | 
    
         
            +
                cl1 = OpenStudio::Point3dVector
         
     | 
| 
      
 1326 
     | 
    
         
            +
                cl2 = OpenStudio::Point3d
         
     | 
| 
      
 1327 
     | 
    
         
            +
                a   = false
         
     | 
| 
      
 1328 
     | 
    
         
            +
                i1  = id1.to_s
         
     | 
| 
      
 1329 
     | 
    
         
            +
                i2  = id2.to_s
         
     | 
| 
      
 1330 
     | 
    
         
            +
                i1  = "poly1" if i1.empty?
         
     | 
| 
      
 1331 
     | 
    
         
            +
                i2  = "poly2" if i2.empty?
         
     | 
| 
      
 1332 
     | 
    
         
            +
             
     | 
| 
      
 1333 
     | 
    
         
            +
                return invalid(i1, mth, 1, DBG, a) unless p1
         
     | 
| 
      
 1334 
     | 
    
         
            +
                valid = p1.is_a?(cl1) || p1.is_a?(Array)
         
     | 
| 
      
 1335 
     | 
    
         
            +
                return mismatch(i1, p1, cl1, mth, DBG, a) unless valid
         
     | 
| 
      
 1336 
     | 
    
         
            +
                return empty(i1, mth, ERR, a) if p1.empty?
         
     | 
| 
      
 1337 
     | 
    
         
            +
             
     | 
| 
      
 1338 
     | 
    
         
            +
                return invalid(i2, mth, 2, DBG, a) unless p2
         
     | 
| 
      
 1339 
     | 
    
         
            +
                valid = p2.is_a?(cl1) || p2.is_a?(Array)
         
     | 
| 
      
 1340 
     | 
    
         
            +
                return mismatch(i2, p2, cl1, mth, DBG, a) unless valid
         
     | 
| 
      
 1341 
     | 
    
         
            +
                return empty(i2, mth, ERR, a) if p2.empty?
         
     | 
| 
      
 1342 
     | 
    
         
            +
             
     | 
| 
      
 1343 
     | 
    
         
            +
                p1.each { |v| return mismatch(i1, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
         
     | 
| 
      
 1344 
     | 
    
         
            +
                p2.each { |v| return mismatch(i2, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
         
     | 
| 
      
 1345 
     | 
    
         
            +
             
     | 
| 
      
 1346 
     | 
    
         
            +
                ft = OpenStudio::Transformation::alignFace(p1).inverse
         
     | 
| 
      
 1347 
     | 
    
         
            +
             
     | 
| 
      
 1348 
     | 
    
         
            +
                ft_p1 = flatZ( (ft * p1).reverse )
         
     | 
| 
      
 1349 
     | 
    
         
            +
                return false if ft_p1.empty?
         
     | 
| 
      
 1350 
     | 
    
         
            +
                area1 = OpenStudio::getArea(ft_p1)
         
     | 
| 
      
 1351 
     | 
    
         
            +
                return empty(i1, mth, ERR, a) if area1.empty?
         
     | 
| 
      
 1352 
     | 
    
         
            +
                area1 = area1.get
         
     | 
| 
      
 1353 
     | 
    
         
            +
             
     | 
| 
      
 1354 
     | 
    
         
            +
                ft_p2 = flatZ( (ft * p2).reverse )
         
     | 
| 
      
 1355 
     | 
    
         
            +
                return false if ft_p2.empty?
         
     | 
| 
      
 1356 
     | 
    
         
            +
                area2 = OpenStudio::getArea(ft_p2)
         
     | 
| 
      
 1357 
     | 
    
         
            +
                return empty(i2, mth, ERR, a) if area2.empty?
         
     | 
| 
      
 1358 
     | 
    
         
            +
                area2 = area2.get
         
     | 
| 
      
 1359 
     | 
    
         
            +
             
     | 
| 
      
 1360 
     | 
    
         
            +
                union = OpenStudio::join(ft_p1, ft_p2, TOL2)
         
     | 
| 
      
 1361 
     | 
    
         
            +
                return false if union.empty?
         
     | 
| 
      
 1362 
     | 
    
         
            +
                union = union.get
         
     | 
| 
      
 1363 
     | 
    
         
            +
                area = OpenStudio::getArea(union)
         
     | 
| 
      
 1364 
     | 
    
         
            +
                return empty("union", mth, ERR, a) if area.empty?
         
     | 
| 
      
 1365 
     | 
    
         
            +
                area = area.get
         
     | 
| 
      
 1366 
     | 
    
         
            +
             
     | 
| 
      
 1367 
     | 
    
         
            +
                return false if area < TOL
         
     | 
| 
      
 1368 
     | 
    
         
            +
                return true if (area - area2).abs < TOL
         
     | 
| 
      
 1369 
     | 
    
         
            +
                return false if (area - area2).abs > TOL
         
     | 
| 
      
 1370 
     | 
    
         
            +
                true
         
     | 
| 
      
 1371 
     | 
    
         
            +
              end
         
     | 
| 
      
 1372 
     | 
    
         
            +
             
     | 
| 
      
 1373 
     | 
    
         
            +
              ##
         
     | 
| 
      
 1374 
     | 
    
         
            +
              # Validate whether an OpenStudio polygon overlaps another.
         
     | 
| 
      
 1375 
     | 
    
         
            +
              #
         
     | 
| 
      
 1376 
     | 
    
         
            +
              # @param p1 [OpenStudio::Point3dVector] or Point3D array of polygon #1
         
     | 
| 
      
 1377 
     | 
    
         
            +
              # @param p2 [OpenStudio::Point3dVector] or Point3D array of polygon #2
         
     | 
| 
      
 1378 
     | 
    
         
            +
              # @param id1 [String] polygon #1 identifier (optional)
         
     | 
| 
      
 1379 
     | 
    
         
            +
              # @param id2 [String] polygon #2 identifier (optional)
         
     | 
| 
      
 1380 
     | 
    
         
            +
              #
         
     | 
| 
      
 1381 
     | 
    
         
            +
              # @return Returns true if polygons overlaps (or either fits into the other)
         
     | 
| 
      
 1382 
     | 
    
         
            +
              # @return [Bool] false if invalid input
         
     | 
| 
      
 1383 
     | 
    
         
            +
              def overlaps?(p1, p2, id1 = "", id2 = "")
         
     | 
| 
      
 1384 
     | 
    
         
            +
                mth = "OSut::#{__callee__}"
         
     | 
| 
      
 1385 
     | 
    
         
            +
                cl1 = OpenStudio::Point3dVector
         
     | 
| 
      
 1386 
     | 
    
         
            +
                cl2 = OpenStudio::Point3d
         
     | 
| 
      
 1387 
     | 
    
         
            +
                a   = false
         
     | 
| 
      
 1388 
     | 
    
         
            +
                i1  = id1.to_s
         
     | 
| 
      
 1389 
     | 
    
         
            +
                i2  = id2.to_s
         
     | 
| 
      
 1390 
     | 
    
         
            +
                i1  = "poly1" if i1.empty?
         
     | 
| 
      
 1391 
     | 
    
         
            +
                i2  = "poly2" if i2.empty?
         
     | 
| 
      
 1392 
     | 
    
         
            +
             
     | 
| 
      
 1393 
     | 
    
         
            +
                return invalid(i1, mth, 1, DBG, a) unless p1
         
     | 
| 
      
 1394 
     | 
    
         
            +
                valid = p1.is_a?(cl1) || p1.is_a?(Array)
         
     | 
| 
      
 1395 
     | 
    
         
            +
                return mismatch(i1, p1, cl1, mth, DBG, a) unless valid
         
     | 
| 
      
 1396 
     | 
    
         
            +
                return empty(i1, mth, ERR, a) if p1.empty?
         
     | 
| 
      
 1397 
     | 
    
         
            +
             
     | 
| 
      
 1398 
     | 
    
         
            +
                return invalid(i2, mth, 2, DBG, a) unless p2
         
     | 
| 
      
 1399 
     | 
    
         
            +
                valid = p2.is_a?(cl1) || p2.is_a?(Array)
         
     | 
| 
      
 1400 
     | 
    
         
            +
                return mismatch(i2, p2, cl1, mth, DBG, a) unless valid
         
     | 
| 
      
 1401 
     | 
    
         
            +
                return empty(i2, mth, ERR, a) if p2.empty?
         
     | 
| 
      
 1402 
     | 
    
         
            +
             
     | 
| 
      
 1403 
     | 
    
         
            +
                p1.each { |v| return mismatch(i1, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
         
     | 
| 
      
 1404 
     | 
    
         
            +
                p2.each { |v| return mismatch(i2, v, cl2, mth, ERR, a) unless v.is_a?(cl2) }
         
     | 
| 
      
 1405 
     | 
    
         
            +
             
     | 
| 
      
 1406 
     | 
    
         
            +
                ft = OpenStudio::Transformation::alignFace(p1).inverse
         
     | 
| 
      
 1407 
     | 
    
         
            +
             
     | 
| 
      
 1408 
     | 
    
         
            +
                ft_p1 = flatZ( (ft * p1).reverse )
         
     | 
| 
      
 1409 
     | 
    
         
            +
                return false if ft_p1.empty?
         
     | 
| 
      
 1410 
     | 
    
         
            +
                area1 = OpenStudio::getArea(ft_p1)
         
     | 
| 
      
 1411 
     | 
    
         
            +
                return empty(i1, mth, ERR, a) if area1.empty?
         
     | 
| 
      
 1412 
     | 
    
         
            +
                area1 = area1.get
         
     | 
| 
      
 1413 
     | 
    
         
            +
             
     | 
| 
      
 1414 
     | 
    
         
            +
                ft_p2 = flatZ( (ft * p2).reverse )
         
     | 
| 
      
 1415 
     | 
    
         
            +
                return false if ft_p2.empty?
         
     | 
| 
      
 1416 
     | 
    
         
            +
                area2 = OpenStudio::getArea(ft_p2)
         
     | 
| 
      
 1417 
     | 
    
         
            +
                return empty(i2, mth, ERR, a) if area2.empty?
         
     | 
| 
      
 1418 
     | 
    
         
            +
                area2 = area2.get
         
     | 
| 
      
 1419 
     | 
    
         
            +
             
     | 
| 
      
 1420 
     | 
    
         
            +
                union = OpenStudio::join(ft_p1, ft_p2, TOL2)
         
     | 
| 
      
 1421 
     | 
    
         
            +
                return false if union.empty?
         
     | 
| 
      
 1422 
     | 
    
         
            +
                union = union.get
         
     | 
| 
      
 1423 
     | 
    
         
            +
                area = OpenStudio::getArea(union)
         
     | 
| 
      
 1424 
     | 
    
         
            +
                return empty("union", mth, ERR, a) if area.empty?
         
     | 
| 
      
 1425 
     | 
    
         
            +
                area = area.get
         
     | 
| 
      
 1426 
     | 
    
         
            +
             
     | 
| 
      
 1427 
     | 
    
         
            +
                return false if area < TOL
         
     | 
| 
      
 1428 
     | 
    
         
            +
                true
         
     | 
| 
      
 1429 
     | 
    
         
            +
              end
         
     | 
| 
      
 1430 
     | 
    
         
            +
            end
         
     |