lib/osut/utils.rb in osut-0.2.2 vs lib/osut/utils.rb in osut-0.2.3

- old
+ new

@@ -33,16 +33,16 @@ module OSut extend OSlg # DEBUG for devs; WARN/ERROR for users (bad OS input) TOL = 0.01 TOL2 = TOL * TOL - NS = "nameString" # OpenStudio IdfObject nameString method DBG = OSut::DEBUG # mainly to flag invalid arguments to devs (buggy code) INF = OSut::INFO # not currently used in OSut WRN = OSut::WARN # WARN users of 'iffy' .osm inputs (yet not critical) ERR = OSut::ERROR # flag invalid .osm inputs (then exit via 'return') FTL = OSut::FATAL # not currently used in OSut + NS = "nameString" # OpenStudio IdfObject nameString method # This first set of utilities (~750 lines) help distinguishing spaces that # are directly vs indirectly CONDITIONED, vs SEMI-HEATED. The solution here # relies as much as possible on space conditioning categories found in # standards like ASHRAE 90.1 and energy codes like the Canadian NECB editions. @@ -125,36 +125,36 @@ # # @param sched [OpenStudio::Model::ScheduleRuleset] schedule # # @return [Hash] min: (Float), max: (Float) # @return [Hash] min: nil, max: nil (if invalid input) - def scheduleRulesetMinMax(sched) + def scheduleRulesetMinMax(sched = nil) # Largely inspired from David Goldwasser's # "schedule_ruleset_annual_min_max_value": # # github.com/NREL/openstudio-standards/blob/ # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/ # standards/Standards.ScheduleRuleset.rb#L124 mth = "OSut::#{__callee__}" cl = OpenStudio::Model::ScheduleRuleset res = { min: nil, max: nil } - return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) + return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) id = sched.nameString return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl) profiles = [] profiles << sched.defaultDaySchedule sched.scheduleRules.each { |rule| profiles << rule.daySchedule } profiles.each do |profile| id = profile.nameString + profile.values.each do |val| - unless val.is_a?(Numeric) - log(WRN, "Skipping non-numeric profile values in '#{id}' (#{mth})") - next - end + ok = val.is_a?(Numeric) + log(WRN, "Skipping non-numeric value in '#{id}' (#{mth})") unless ok + next unless ok res[:min] = val unless res[:min] res[:min] = val if res[:min] > val res[:max] = val unless res[:max] res[:max] = val if res[:max] < val @@ -172,31 +172,29 @@ # # @param sched [OpenStudio::Model::ScheduleConstant] schedule # # @return [Hash] min: (Float), max: (Float) # @return [Hash] min: nil, max: nil (if invalid input) - def scheduleConstantMinMax(sched) + def scheduleConstantMinMax(sched = nil) # Largely inspired from David Goldwasser's # "schedule_constant_annual_min_max_value": # # github.com/NREL/openstudio-standards/blob/ # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/ # standards/Standards.ScheduleConstant.rb#L21 mth = "OSut::#{__callee__}" cl = OpenStudio::Model::ScheduleConstant res = { min: nil, max: nil } - return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) + return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) id = sched.nameString return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl) - unless sched.value.is_a?(Numeric) - return mismatch("'#{id}' value", sched.value, Numeric, mth, ERR, res) - else - res[:min] = sched.value - res[:max] = sched.value - end + valid = sched.value.is_a?(Numeric) + mismatch("'#{id}' value", sched.value, Numeric, mth, ERR, res) unless valid + res[:min] = sched.value + res[:max] = sched.value res end ## @@ -204,11 +202,11 @@ # # @param sched [OpenStudio::Model::ScheduleCompact] schedule # # @return [Hash] min: (Float), max: (Float) # @return [Hash] min: nil, max: nil (if invalid input) - def scheduleCompactMinMax(sched) + def scheduleCompactMinMax(sched = nil) # Largely inspired from Andrew Parker's # "schedule_compact_annual_min_max_value": # # github.com/NREL/openstudio-standards/blob/ # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/ @@ -217,11 +215,11 @@ cl = OpenStudio::Model::ScheduleCompact vals = [] prev_str = "" res = { min: nil, max: nil } - return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) + return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) id = sched.nameString return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl) sched.extensibleGroups.each do |eg| if prev_str.include?("until") @@ -231,46 +229,42 @@ str = eg.getString(0) prev_str = str.get.downcase unless str.empty? end return empty("'#{id}' values", mth, ERR, res) if vals.empty? + ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric) + log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok + return res unless ok + res[:min] = vals.min + res[:max] = vals.max - if vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric) - res[:min] = vals.min - res[:max] = vals.max - else - log(ERR, "Non-numeric values in '#{id}' (#{mth})") - end - res end ## # Return min & max values for schedule (interval). # # @param sched [OpenStudio::Model::ScheduleInterval] schedule # # @return [Hash] min: (Float), max: (Float) # @return [Hash] min: nil, max: nil (if invalid input) - def scheduleIntervalMinMax(sched) + def scheduleIntervalMinMax(sched = nil) mth = "OSut::#{__callee__}" cl = OpenStudio::Model::ScheduleInterval vals = [] prev_str = "" res = { min: nil, max: nil } - return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) + return invalid("sched", mth, 1, DBG, res) unless sched.respond_to?(NS) id = sched.nameString return mismatch(id, sched, cl, mth, DBG, res) unless sched.is_a?(cl) - vals = sched.timeSeries.values - if vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric) - res[:min] = vals.min - res[:max] = vals.max - else - log(ERR, "Non-numeric values in '#{id}' (#{mth})") - end + ok = vals.min.is_a?(Numeric) && vals.max.is_a?(Numeric) + log(ERR, "Non-numeric values in '#{id}' (#{mth})") unless ok + return res unless ok + res[:min] = vals.min + res[:max] = vals.max res end ## @@ -279,11 +273,11 @@ # # @param zone [OpenStudio::Model::ThermalZone] a thermal zone # # @return [Hash] spt: (Float), dual: (Bool) # @return [Hash] spt: nil, dual: false (if invalid input) - def maxHeatScheduledSetpoint(zone) + def maxHeatScheduledSetpoint(zone = nil) # Largely inspired from Parker & Marrec's "thermal_zone_heated?" procedure. # The solution here is a tad more relaxed to encompass SEMI-HEATED zones as # per Canadian NECB criteria (basically any space with at least 10 W/m2 of # installed heating equipement, i.e. below freezing in Canada). # @@ -292,11 +286,11 @@ # standards/Standards.ThermalZone.rb#L910 mth = "OSut::#{__callee__}" cl = OpenStudio::Model::ThermalZone res = { spt: nil, dual: false } - return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS) + return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS) id = zone.nameString return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl) # Zone radiant heating? Get schedule from radiant system. zone.equipment.each do |equip| @@ -308,13 +302,20 @@ unless equip.heatingSetpointTemperatureSchedule.empty? sched = equip.heatingSetpointTemperatureSchedule.get end end + # unless equip.to_ZoneHVACLowTemperatureRadiantElectric.empty? + # equip = equip.to_ZoneHVACLowTemperatureRadiantElectric.get + # + # unless equip.heatingSetpointTemperatureSchedule.empty? + # sched = equip.heatingSetpointTemperatureSchedule.get + # end + # end + unless equip.to_ZoneHVACLowTemperatureRadiantElectric.empty? equip = equip.to_ZoneHVACLowTemperatureRadiantElectric.get - unless equip.heatingSetpointTemperatureSchedule.empty? sched = equip.heatingSetpointTemperatureSchedule.get end end @@ -375,26 +376,26 @@ res[:spt] = max if res[:spt] < max end end end - return res if res[:spt] return res if zone.thermostat.empty? - tstat = zone.thermostat.get + tstat = zone.thermostat.get + res[:spt] = nil unless tstat.to_ThermostatSetpointDualSetpoint.empty? && tstat.to_ZoneControlThermostatStagedDualSetpoint.empty? - res[:dual] = true unless tstat.to_ThermostatSetpointDualSetpoint.empty? tstat = tstat.to_ThermostatSetpointDualSetpoint.get else tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get end unless tstat.heatingSetpointTemperatureSchedule.empty? - sched = tstat.heatingSetpointTemperatureSchedule.get + res[:dual] = true + sched = tstat.heatingSetpointTemperatureSchedule.get unless sched.to_ScheduleRuleset.empty? sched = sched.to_ScheduleRuleset.get max = scheduleRulesetMinMax(sched)[:max] @@ -454,15 +455,14 @@ # # @param model [OpenStudio::Model::Model] a model # # @return [Bool] true if valid heating temperature setpoints # @return [Bool] false if invalid input - def heatingTemperatureSetpoints?(model) + def heatingTemperatureSetpoints?(model = nil) mth = "OSut::#{__callee__}" - cl = OpenStudio::Model::Model + cl = OpenStudio::Model::Model - return invalid("model", mth, 1, DBG, false) unless model return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl) model.getThermalZones.each do |zone| return true if maxHeatScheduledSetpoint(zone)[:spt] end @@ -476,21 +476,21 @@ # # @param zone [OpenStudio::Model::ThermalZone] a thermal zone # # @return [Hash] spt: (Float), dual: (Bool) # @return [Hash] spt: nil, dual: false (if invalid input) - def minCoolScheduledSetpoint(zone) + def minCoolScheduledSetpoint(zone = nil) # Largely inspired from Parker & Marrec's "thermal_zone_cooled?" procedure. # # github.com/NREL/openstudio-standards/blob/ # 99cf713750661fe7d2082739f251269c2dfd9140/lib/openstudio-standards/ # standards/Standards.ThermalZone.rb#L1058 mth = "OSut::#{__callee__}" cl = OpenStudio::Model::ThermalZone res = { spt: nil, dual: false } - return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS) + return invalid("zone", mth, 1, DBG, res) unless zone.respond_to?(NS) id = zone.nameString return mismatch(id, zone, cl, mth, DBG, res) unless zone.is_a?(cl) # Zone radiant cooling? Get schedule from radiant system. zone.equipment.each do |equip| @@ -553,26 +553,26 @@ res[:spt] = min if res[:spt] > min end end end - return res if res[:spt] return res if zone.thermostat.empty? - tstat = zone.thermostat.get + tstat = zone.thermostat.get + res[:spt] = nil unless tstat.to_ThermostatSetpointDualSetpoint.empty? && tstat.to_ZoneControlThermostatStagedDualSetpoint.empty? - res[:dual] = true unless tstat.to_ThermostatSetpointDualSetpoint.empty? tstat = tstat.to_ThermostatSetpointDualSetpoint.get else tstat = tstat.to_ZoneControlThermostatStagedDualSetpoint.get end unless tstat.coolingSetpointTemperatureSchedule.empty? - sched = tstat.coolingSetpointTemperatureSchedule.get + res[:dual] = true + sched = tstat.coolingSetpointTemperatureSchedule.get unless sched.to_ScheduleRuleset.empty? sched = sched.to_ScheduleRuleset.get min = scheduleRulesetMinMax(sched)[:min] @@ -632,15 +632,14 @@ # # @param model [OpenStudio::Model::Model] a model # # @return [Bool] true if valid cooling temperature setpoints # @return [Bool] false if invalid input - def coolingTemperatureSetpoints?(model) + def coolingTemperatureSetpoints?(model = nil) mth = "OSut::#{__callee__}" - cl = OpenStudio::Model::Model + cl = OpenStudio::Model::Model - return invalid("model", mth, 1, DBG, false) unless model return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl) model.getThermalZones.each do |zone| return true if minCoolScheduledSetpoint(zone)[:spt] end @@ -653,15 +652,14 @@ # # @param model [OpenStudio::Model::Model] a model # # @return [Bool] true if model has one or more HVAC air loops # @return [Bool] false if invalid input - def airLoopsHVAC?(model) + def airLoopsHVAC?(model = nil) mth = "OSut::#{__callee__}" - cl = OpenStudio::Model::Model + cl = OpenStudio::Model::Model - return invalid("model", mth, 1, DBG, false) unless model return mismatch("model", model, cl, mth, DBG, false) unless model.is_a?(cl) model.getThermalZones.each do |zone| next if zone.canBePlenum return true unless zone.airLoopHVACs.empty? @@ -678,93 +676,83 @@ # @param loops [Bool] true if model has airLoopHVAC object(s) # @param setpoints [Bool] true if model has valid temperature setpoints # # @return [Bool] true if should be tagged as plenum # @return [Bool] false if invalid input - def plenum?(space, loops, setpoints) + def plenum?(space = nil, loops = nil, setpoints = nil) # Largely inspired from NREL's "space_plenum?" procedure: # # github.com/NREL/openstudio-standards/blob/ # 58964222d25783e9da4ae292e375fb0d5c902aa5/lib/openstudio-standards/ # standards/Standards.Space.rb#L1384 - + # # A space may be tagged as a plenum if: # # CASE A: its zone's "isPlenum" == true (SDK method) for a fully-developed # OpenStudio model (complete with HVAC air loops); # - # CASE B: it's excluded from building's total floor area yet linked to a - # zone holding an "inactive" thermostat (i.e., can't extract - # valid setpoints); + # CASE B: (IN ABSENCE OF HVAC AIRLOOPS) if it's excluded from a building's + # total floor area yet linked to a zone holding an 'inactive' + # thermostat, i.e. can't extract valid setpoints; OR # - # CASE C: it has a spacetype whose name holds "plenum", or a spacetype with - # a 'standards spacetype' holding "plenum" (case insensitive); OR - # - # CASE D: its name string holds "plenum" (also case insensitive). - + # CASE C: (IN ABSENCE OF HVAC AIRLOOPS & VALID SETPOINTS) it has "plenum" + # (case insensitive) as a spacetype (or as a spacetype's + # 'standards spacetype'). mth = "OSut::#{__callee__}" cl = OpenStudio::Model::Space - return invalid("space", mth, 1, DBG, false) unless space.respond_to?(NS) + return invalid("space", mth, 1, DBG, false) unless space.respond_to?(NS) id = space.nameString return mismatch(id, space, cl, mth, DBG, false) unless space.is_a?(cl) - valid = loops == true || loops == false - return invalid("loops", mth, 2, DBG, false) unless valid - + return invalid("loops", mth, 2, DBG, false) unless valid valid = setpoints == true || setpoints == false return invalid("setpoints", mth, 3, DBG, false) unless valid unless space.thermalZone.empty? zone = space.thermalZone.get - return true if zone.isPlenum && loops # CASE A + return zone.isPlenum if loops # A if setpoints - heating = maxHeatScheduledSetpoint(zone) - cooling = minCoolScheduledSetpoint(zone) - - return false if heating[:spt] || cooling[:spt] # directly conditioned - - unless space.partofTotalFloorArea - return true if heating[:dual] || cooling[:dual] # CASE B - end + heat = maxHeatScheduledSetpoint(zone) + cool = minCoolScheduledSetpoint(zone) + return false if heat[:spt] || cool[:spt] # directly conditioned + return heat[:dual] || cool[:dual] unless space.partofTotalFloorArea # B + return false end end unless space.spaceType.empty? type = space.spaceType.get - return true if type.nameString.downcase.include?("plenum") # CASE C + return type.nameString.downcase == "plenum" # C unless type.standardsSpaceType.empty? type = type.standardsSpaceType.get - return true if type.downcase.include?("plenum") # CASE C + return type.downcase == "plenum" # C end end - return true if space.nameString.downcase.include?("plenum") - false end ## # Generate an HVAC availability schedule. # # @param model [OpenStudio::Model::Model] a model # @param avl [String] seasonal availability choice (optional, default "ON") # # @return [OpenStudio::Model::Schedule] HVAC availability sched - # @return [nil] if invalid input - def availabilitySchedule(model, avl = "") - mth = "OSut::#{__callee__}" - cl = OpenStudio::Model::Model + # @return [NilClass] if invalid input + def availabilitySchedule(model = nil, avl = "") + mth = "OSut::#{__callee__}" + cl = OpenStudio::Model::Model + limits = nil - return invalid("model", mth, 1) unless model - return mismatch("model", model, cl, mth) unless model.is_a?(cl) + return mismatch("model", model, cl, mth) unless model.is_a?(cl) + return invalid("availability", avl, 2, mth) unless avl.respond_to?(:to_s) # Either fetch availability ScheduleTypeLimits object, or create one. - limits = nil - model.getScheduleTypeLimitss.each do |l| break if limits next if l.lowerLimitValue.empty? next if l.upperLimitValue.empty? next if l.numericType.empty? @@ -795,11 +783,11 @@ return empty("yearDescription", mth, ERR) if year.empty? year = year.get may01 = year.makeDate(OpenStudio::MonthOfYear.new("May"), 1) oct31 = year.makeDate(OpenStudio::MonthOfYear.new("Oct"), 31) - case avl.downcase + case avl.to_s.downcase when "winter" # available from November 1 to April 30 (6 months) val = 1 sch = off nom = "WINTER Availability SchedRuleset" dft = "WINTER Availability dftDaySched" @@ -836,120 +824,106 @@ schedule = schedule.get.to_ScheduleRuleset unless schedule.empty? schedule = schedule.get default = schedule.defaultDaySchedule - ok = ok && default.nameString == dft - ok = ok && default.times.size == 1 - ok = ok && default.values.size == 1 - ok = ok && default.times.first == time - ok = ok && default.values.first == val + ok = ok && default.nameString == dft + ok = ok && default.times.size == 1 + ok = ok && default.values.size == 1 + ok = ok && default.times.first == time + ok = ok && default.values.first == val rules = schedule.scheduleRules ok = ok && (rules.size == 0 || rules.size == 1) if rules.size == 1 rule = rules.first - ok = ok && rule.nameString == tag + ok = ok && rule.nameString == tag ok = ok && !rule.startDate.empty? ok = ok && !rule.endDate.empty? - ok = ok && rule.startDate.get == may01 - ok = ok && rule.endDate.get == oct31 + ok = ok && rule.startDate.get == may01 + ok = ok && rule.endDate.get == oct31 ok = ok && rule.applyAllDays d = rule.daySchedule - ok = ok && d.nameString == day - ok = ok && d.times.size == 1 - ok = ok && d.values.size == 1 + ok = ok && d.nameString == day + ok = ok && d.times.size == 1 + ok = ok && d.values.size == 1 ok = ok && d.times.first.totalSeconds == secs - ok = ok && d.values.first.to_i != val + ok = ok && d.values.first.to_i != val end return schedule if ok end end schedule = OpenStudio::Model::ScheduleRuleset.new(model) schedule.setName(nom) - - unless schedule.setScheduleTypeLimits(limits) - log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})") - return nil - end - - unless schedule.defaultDaySchedule.addValue(time, val) - log(ERR, "'#{nom}': Can't set default day schedule (#{mth})") - return nil - end - + ok = schedule.setScheduleTypeLimits(limits) + log(ERR, "'#{nom}': Can't set schedule type limits (#{mth})") unless ok + return nil unless ok + ok = schedule.defaultDaySchedule.addValue(time, val) + log(ERR, "'#{nom}': Can't set default day schedule (#{mth})") unless ok + return nil unless ok schedule.defaultDaySchedule.setName(dft) unless tag.empty? rule = OpenStudio::Model::ScheduleRule.new(schedule, sch) rule.setName(tag) - - unless rule.setStartDate(may01) - log(ERR, "'#{tag}': Can't set start date (#{mth})") - return nil - end - - unless rule.setEndDate(oct31) - log(ERR, "'#{tag}': Can't set end date (#{mth})") - return nil - end - - unless rule.setApplyAllDays(true) - log(ERR, "'#{tag}': Can't apply to all days (#{mth})") - return nil - end - + ok = rule.setStartDate(may01) + log(ERR, "'#{tag}': Can't set start date (#{mth})") unless ok + return nil unless ok + ok = rule.setEndDate(oct31) + log(ERR, "'#{tag}': Can't set end date (#{mth})") unless ok + return nil unless ok + ok = rule.setApplyAllDays(true) + log(ERR, "'#{tag}': Can't apply to all days (#{mth})") unless ok + return nil unless ok rule.daySchedule.setName(day) end schedule end ## # Validate if default construction set holds a base ground construction. # # @param set [OpenStudio::Model::DefaultConstructionSet] a default set - # @param base [OpensStudio::Model::ConstructionBase] a construction base - # @param ground [Bool] true if ground-facing surface - # @param exterior [Bool] true if exterior-facing surface - # @param type [String] a surface type + # @param bse [OpensStudio::Model::ConstructionBase] a construction base + # @param gr [Bool] true if ground-facing surface + # @param ex [Bool] true if exterior-facing surface + # @param typ [String] a surface type # # @return [Bool] true if default construction set holds construction # @return [Bool] false if invalid input - def holdsConstruction?(set, base, ground = false, exterior = false, type = "") + def holdsConstruction?(set = nil, bse = nil, gr = false, ex = false, typ = "") mth = "OSut::#{__callee__}" cl1 = OpenStudio::Model::DefaultConstructionSet cl2 = OpenStudio::Model::ConstructionBase - return invalid("set", mth, 1, DBG, false) unless set.respond_to?(NS) + return invalid("set", mth, 1, DBG, false) unless set.respond_to?(NS) id = set.nameString - return mismatch(id, set, cl1, mth, DBG, false) unless set.is_a?(cl1) - - return invalid("base", mth, 2, DBG, false) unless base.respond_to?(NS) - id = base.nameString - return mismatch(id, base, cl2, mth, DBG, false) unless base.is_a?(cl2) - - valid = ground == true || ground == false - return invalid("ground", mth, 3, DBG, false) unless valid - - valid = exterior == true || exterior == false - return invalid("exterior", mth, 4, DBG, false) unless valid - - typ = type.to_s.downcase - valid = typ == "floor" || typ == "wall" || typ == "roofceiling" + return mismatch(id, set, cl1, mth, DBG, false) unless set.is_a?(cl1) + return invalid("base", mth, 2, DBG, false) unless bse.respond_to?(NS) + id = bse.nameString + return mismatch(id, bse, cl2, mth, DBG, false) unless bse.is_a?(cl2) + valid = gr == true || gr == false + return invalid("ground", mth, 3, DBG, false) unless valid + valid = ex == true || ex == false + return invalid("exterior", mth, 4, DBG, false) unless valid + valid = typ.respond_to?(:to_s) + return invalid("surface typ", mth, 4, DBG, false) unless valid + type = typ.to_s.downcase + valid = type == "floor" || type == "wall" || type == "roofceiling" return invalid("surface type", mth, 5, DBG, false) unless valid constructions = nil - if ground + if gr unless set.defaultGroundContactSurfaceConstructions.empty? constructions = set.defaultGroundContactSurfaceConstructions.get end - elsif exterior + elsif ex unless set.defaultExteriorSurfaceConstructions.empty? constructions = set.defaultExteriorSurfaceConstructions.get end else unless set.defaultInteriorSurfaceConstructions.empty? @@ -957,25 +931,25 @@ end end return false unless constructions - case typ + case type when "roofceiling" unless constructions.roofCeilingConstruction.empty? construction = constructions.roofCeilingConstruction.get - return true if construction == base + return true if construction == bse end when "floor" unless constructions.floorConstruction.empty? construction = constructions.floorConstruction.get - return true if construction == base + return true if construction == bse end else unless constructions.wallConstruction.empty? construction = constructions.wallConstruction.get - return true if construction == base + return true if construction == bse end end false end @@ -985,34 +959,29 @@ # # @param model [OpenStudio::Model::Model] a model # @param s [OpenStudio::Model::Surface] a surface # # @return [OpenStudio::Model::DefaultConstructionSet] default set - # @return [nil] if invalid input - def defaultConstructionSet(model, s) + # @return [NilClass] if invalid input + def defaultConstructionSet(model = nil, s = nil) mth = "OSut::#{__callee__}" cl1 = OpenStudio::Model::Model cl2 = OpenStudio::Model::Surface - return invalid("model", mth, 1) unless model return mismatch("model", model, cl1, mth) unless model.is_a?(cl1) - - return invalid("s", mth, 2) unless s.respond_to?(NS) + return invalid("s", mth, 2) unless s.respond_to?(NS) id = s.nameString - return mismatch(id, s, cl2, mth) unless s.is_a?(cl2) + return mismatch(id, s, cl2, mth) unless s.is_a?(cl2) - unless s.isConstructionDefaulted - log(ERR, "'#{id}' construction not defaulted (#{mth})") - return nil - end - + ok = s.isConstructionDefaulted + log(ERR, "'#{id}' construction not defaulted (#{mth})") unless ok + return nil unless ok return empty("'#{id}' construction", mth, ERR) if s.construction.empty? base = s.construction.get return empty("'#{id}' space", mth, ERR) if s.space.empty? space = s.space.get type = s.surfaceType - ground = false exterior = false if s.isGroundSurface ground = true @@ -1058,43 +1027,43 @@ # # @param lc [OpenStudio::LayeredConstruction] a layered construction # # @return [Bool] true if all layers are valid # @return [Bool] false if invalid input - def standardOpaqueLayers?(lc) + def standardOpaqueLayers?(lc = nil) mth = "OSut::#{__callee__}" cl = OpenStudio::Model::LayeredConstruction return invalid("lc", mth, 1, DBG, false) unless lc.respond_to?(NS) return mismatch(lc.nameString, lc, cl, mth, DBG, false) unless lc.is_a?(cl) lc.layers.each { |m| return false if m.to_StandardOpaqueMaterial.empty? } + true end ## # Total (standard opaque) layered construction thickness (in m). # # @param lc [OpenStudio::LayeredConstruction] a layered construction # # @return [Double] total layered construction thickness # @return [Double] 0 if invalid input - def thickness(lc) + def thickness(lc = nil) mth = "OSut::#{__callee__}" cl = OpenStudio::Model::LayeredConstruction - return invalid("lc", mth, 1, DBG, 0) unless lc.respond_to?(NS) + return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS) id = lc.nameString - return mismatch(id, lc, cl, mth, DBG, 0) unless lc.is_a?(cl) + return mismatch(id, lc, cl, mth, DBG, 0.0) unless lc.is_a?(cl) - unless standardOpaqueLayers?(lc) - log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") - return 0 - end - + ok = standardOpaqueLayers?(lc) + log(ERR, "'#{id}' holds non-StandardOpaqueMaterial(s) (#{mth})") unless ok + return 0.0 unless ok thickness = 0.0 lc.layers.each { |m| thickness += m.thickness } + thickness end ## # Return total air film resistance for fenestration. @@ -1120,15 +1089,16 @@ # layer thickness of 2mm & k = ~0.6 W/m.K. # # The EnergyPlus Engineering calculations were designed for vertical windows # - not horizontal, slanted or domed surfaces - use with caution. mth = "OSut::#{__callee__}" - cl = Numeric + cl = Numeric - return invalid("usi", mth, 1, DBG, 0.1216) unless usi - return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl) - return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0 + return mismatch("usi", usi, cl, mth, DBG, 0.1216) unless usi.is_a?(cl) + return invalid("usi", mth, 1, WRN, 0.1216) if usi > 8.0 + return negative("usi", mth, WRN, 0.1216) if usi < 0 + return zero("usi", mth, WRN, 0.1216) if usi.abs < TOL rsi = 1 / (0.025342 * usi + 29.163853) # exterior film, next interior film return rsi + 1 / (0.359073 * Math.log(usi) + 6.949915) if usi < 5.85 return rsi + 1 / (1.788041 * usi - 2.886625) @@ -1140,68 +1110,53 @@ # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction # @param film [Float] thermal resistance of surface air films (m2•K/W) # @param t [Float] gas temperature (°C) (optional) # # @return [Float] calculated RSi at standard conditions (0 if error) - def rsi(lc, film, t = 0.0) + def rsi(lc = nil, film = 0.0, t = 0.0) # This is adapted from BTAP's Material Module's "get_conductance" (P. Lopez) # # https://github.com/NREL/OpenStudio-Prototype-Buildings/blob/ # c3d5021d8b7aef43e560544699fb5c559e6b721d/lib/btap/measures/ # btap_equest_converter/envelope.rb#L122 - mth = "OSut::#{__callee__}" cl1 = OpenStudio::Model::LayeredConstruction cl2 = Numeric - return invalid("lc", mth, 1, DBG, 0) unless lc.respond_to?(NS) + return invalid("lc", mth, 1, DBG, 0.0) unless lc.respond_to?(NS) id = lc.nameString - return mismatch(id, lc, cl1, mth, DBG, 0) unless lc.is_a?(cl1) + return mismatch(id, lc, cl1, mth, DBG, 0.0) unless lc.is_a?(cl1) + return mismatch("film", film, cl2, mth, DBG, 0.0) unless film.is_a?(cl2) + return mismatch("temp K", t, cl2, mth, DBG, 0.0) unless t.is_a?(cl2) + t += 273.0 # °C to K + return negative("temp K", mth, DBG, 0.0) if t < 0 + return negative("film", mth, DBG, 0.0) if film < 0 - return invalid("film", mth, 2, DBG, 0) unless film - return invalid("temperature", mth, 3, DBG, 0) unless t - - return mismatch("film", film, cl2, mth, DBG, 0) unless film.is_a?(cl2) - return mismatch("temperature", t, cl2, mth, DBG, 0) unless t.is_a?(cl2) - - tt = t + 273.0 # °C to K - return negative("temp K", mth, DBG, 0) if tt < 0 - return negative("film", mth, DBG, 0) if film < 0 - rsi = film lc.layers.each do |m| # Fenestration materials first (ignoring shades, screens, etc.) - unless m.to_SimpleGlazing.empty? - return 1 / m.to_SimpleGlazing.get.uFactor # no need to loop - end - unless m.to_StandardGlazing.empty? - rsi += m.to_StandardGlazing.get.thermalResistance - end - unless m.to_RefractionExtinctionGlazing.empty? - rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance - end - unless m.to_Gas.empty? - rsi += m.to_Gas.get.getThermalResistance(tt) - end - unless m.to_GasMixture.empty? - rsi += m.to_GasMixture.get.getThermalResistance(tt) - end + empty = m.to_SimpleGlazing.empty? + return 1 / m.to_SimpleGlazing.get.uFactor unless empty + empty = m.to_StandardGlazing.empty? + rsi += m.to_StandardGlazing.get.thermalResistance unless empty + empty = m.to_RefractionExtinctionGlazing.empty? + rsi += m.to_RefractionExtinctionGlazing.get.thermalResistance unless empty + empty = m.to_Gas.empty? + rsi += m.to_Gas.get.getThermalResistance(t) unless empty + empty = m.to_GasMixture.empty? + rsi += m.to_GasMixture.get.getThermalResistance(t) unless empty # Opaque materials next. - unless m.to_StandardOpaqueMaterial.empty? - rsi += m.to_StandardOpaqueMaterial.get.thermalResistance - end - unless m.to_MasslessOpaqueMaterial.empty? - rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance - end - unless m.to_RoofVegetation.empty? - rsi += m.to_RoofVegetation.get.thermalResistance - end - unless m.to_AirGap.empty? - rsi += m.to_AirGap.get.thermalResistance - end + empty = m.to_StandardOpaqueMaterial.empty? + rsi += m.to_StandardOpaqueMaterial.get.thermalResistance unless empty + empty = m.to_MasslessOpaqueMaterial.empty? + rsi += m.to_MasslessOpaqueMaterial.get.thermalResistance unless empty + empty = m.to_RoofVegetation.empty? + rsi += m.to_RoofVegetation.get.thermalResistance unless empty + empty = m.to_AirGap.empty? + rsi += m.to_AirGap.get.thermalResistance unless empty end rsi end @@ -1213,22 +1168,21 @@ # # @param lc [OpenStudio::Model::LayeredConstruction] a layered construction # # @return [Hash] index: (Integer), type: (:standard or :massless), r: (Float) # @return [Hash] index: nil, type: nil, r: 0 (if invalid input) - def insulatingLayer(lc) + def insulatingLayer(lc = nil) mth = "OSut::#{__callee__}" cl = OpenStudio::Model::LayeredConstruction res = { index: nil, type: nil, r: 0.0 } i = 0 # iterator - return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS) + return invalid("lc", mth, 1, DBG, res) unless lc.respond_to?(NS) id = lc.nameString return mismatch(id, lc, cl1, mth, DBG, res) unless lc.is_a?(cl) lc.layers.each do |m| - unless m.to_MasslessOpaqueMaterial.empty? m = m.to_MasslessOpaqueMaterial.get if m.thermalResistance < 0.001 || m.thermalResistance < res[:r] i += 1 @@ -1267,19 +1221,17 @@ # @param model [OpenStudio::Model::Model] a model # @param group [OpenStudio::Model::PlanarSurfaceGroup] a group # # @return [Hash] t: (OpenStudio::Transformation), r: Float # @return [Hash] t: nil, r: nil (if invalid input) - def transforms(model, group) + def transforms(model = nil, group = nil) mth = "OSut::#{__callee__}" cl1 = OpenStudio::Model::Model cl2 = OpenStudio::Model::PlanarSurfaceGroup res = { t: nil, r: nil } - return invalid("model", mth, 1, DBG, res) unless model return mismatch("model", model, cl1, mth, DBG, res) unless model.is_a?(cl1) - return invalid("group", mth, 2, DBG, res) unless group.respond_to?(NS) id = group.nameString return mismatch(id, group, cl2, mth, DBG, res) unless group.is_a?(cl2) res[:t] = group.siteTransformation @@ -1292,20 +1244,18 @@ # Flatten OpenStudio 3D points vs Z-axis (Z=0). # # @param pts [Array] an OpenStudio Point3D array/vector # # @return [Array] flattened OpenStudio 3D points - def flatZ(pts) + def flatZ(pts = nil) mth = "OSut::#{__callee__}" cl1 = OpenStudio::Point3dVector cl2 = OpenStudio::Point3d - v = OpenStudio::Point3dVector.new + v = OpenStudio::Point3dVector.new - return invalid("points", mth, 1, DBG, v) unless pts valid = pts.is_a?(cl1) || pts.is_a?(Array) - return mismatch("points", pts, cl1, mth, DBG, v) unless valid - + return mismatch("points", pts, cl1, mth, DBG, v) unless valid pts.each { |pt| mismatch("pt", pt, cl2, mth, ERR, v) unless pt.is_a?(cl2) } pts.each { |pt| v << OpenStudio::Point3d.new(pt.x, pt.y, 0) } v end @@ -1318,57 +1268,53 @@ # @param id1 [String] polygon #1 identifier (optional) # @param id2 [String] polygon #2 identifier (optional) # # @return [Bool] true if 1st polygon fits entirely within the 2nd polygon # @return [Bool] false if invalid input - def fits?(p1, p2, id1 = "", id2 = "") + def fits?(p1 = nil, p2 = nil, id1 = "", id2 = "") mth = "OSut::#{__callee__}" cl1 = OpenStudio::Point3dVector cl2 = OpenStudio::Point3d a = false + + return invalid("id1", mth, 3, DBG, a) unless id1.respond_to?(:to_s) + return invalid("id2", mth, 4, DBG, a) unless id2.respond_to?(:to_s) i1 = id1.to_s i2 = id2.to_s i1 = "poly1" if i1.empty? i2 = "poly2" if i2.empty? - - return invalid(i1, mth, 1, DBG, a) unless p1 - valid = p1.is_a?(cl1) || p1.is_a?(Array) - return mismatch(i1, p1, cl1, mth, DBG, a) unless valid + valid1 = p1.is_a?(cl1) || p1.is_a?(Array) + valid2 = p2.is_a?(cl1) || p2.is_a?(Array) + return mismatch(i1, p1, cl1, mth, DBG, a) unless valid1 + return mismatch(i2, p2, cl1, mth, DBG, a) unless valid2 return empty(i1, mth, ERR, a) if p1.empty? - - return invalid(i2, mth, 2, DBG, a) unless p2 - valid = p2.is_a?(cl1) || p2.is_a?(Array) - return mismatch(i2, p2, cl1, mth, DBG, a) unless valid return empty(i2, mth, ERR, a) if p2.empty? - p1.each { |v| return mismatch(i1, v, cl2, mth, ERR, a) unless v.is_a?(cl2) } p2.each { |v| return mismatch(i2, v, cl2, mth, ERR, a) unless v.is_a?(cl2) } ft = OpenStudio::Transformation::alignFace(p1).inverse - ft_p1 = flatZ( (ft * p1).reverse ) return false if ft_p1.empty? area1 = OpenStudio::getArea(ft_p1) - return empty(i1, mth, ERR, a) if area1.empty? + return empty("#{i1} area", mth, ERR, a) if area1.empty? area1 = area1.get - ft_p2 = flatZ( (ft * p2).reverse ) return false if ft_p2.empty? area2 = OpenStudio::getArea(ft_p2) - return empty(i2, mth, ERR, a) if area2.empty? + return empty("#{i2} area", mth, ERR, a) if area2.empty? area2 = area2.get - union = OpenStudio::join(ft_p1, ft_p2, TOL2) return false if union.empty? union = union.get area = OpenStudio::getArea(union) - return empty("union", mth, ERR, a) if area.empty? + return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty? area = area.get return false if area < TOL return true if (area - area2).abs < TOL return false if (area - area2).abs > TOL + true end ## # Validate whether an OpenStudio polygon overlaps another. @@ -1378,53 +1324,57 @@ # @param id1 [String] polygon #1 identifier (optional) # @param id2 [String] polygon #2 identifier (optional) # # @return Returns true if polygons overlaps (or either fits into the other) # @return [Bool] false if invalid input - def overlaps?(p1, p2, id1 = "", id2 = "") + def overlaps?(p1 = nil, p2 = nil, id1 = "", id2 = "") mth = "OSut::#{__callee__}" cl1 = OpenStudio::Point3dVector cl2 = OpenStudio::Point3d a = false - i1 = id1.to_s - i2 = id2.to_s - i1 = "poly1" if i1.empty? - i2 = "poly2" if i2.empty? - return invalid(i1, mth, 1, DBG, a) unless p1 - valid = p1.is_a?(cl1) || p1.is_a?(Array) - return mismatch(i1, p1, cl1, mth, DBG, a) unless valid + return invalid("id1", mth, 3, DBG, a) unless id1.respond_to?(:to_s) + return invalid("id2", mth, 4, DBG, a) unless id2.respond_to?(:to_s) + i1 = id1.to_s + i2 = id2.to_s + i1 = "poly1" if i1.empty? + i2 = "poly2" if i2.empty? + valid1 = p1.is_a?(cl1) || p1.is_a?(Array) + valid2 = p2.is_a?(cl1) || p2.is_a?(Array) + return mismatch(i1, p1, cl1, mth, DBG, a) unless valid1 + return mismatch(i2, p2, cl1, mth, DBG, a) unless valid2 return empty(i1, mth, ERR, a) if p1.empty? - - return invalid(i2, mth, 2, DBG, a) unless p2 - valid = p2.is_a?(cl1) || p2.is_a?(Array) - return mismatch(i2, p2, cl1, mth, DBG, a) unless valid return empty(i2, mth, ERR, a) if p2.empty? - p1.each { |v| return mismatch(i1, v, cl2, mth, ERR, a) unless v.is_a?(cl2) } p2.each { |v| return mismatch(i2, v, cl2, mth, ERR, a) unless v.is_a?(cl2) } ft = OpenStudio::Transformation::alignFace(p1).inverse - ft_p1 = flatZ( (ft * p1).reverse ) return false if ft_p1.empty? area1 = OpenStudio::getArea(ft_p1) - return empty(i1, mth, ERR, a) if area1.empty? + return empty("#{i1} area", mth, ERR, a) if area1.empty? area1 = area1.get - ft_p2 = flatZ( (ft * p2).reverse ) return false if ft_p2.empty? area2 = OpenStudio::getArea(ft_p2) - return empty(i2, mth, ERR, a) if area2.empty? + return empty("#{i2} area", mth, ERR, a) if area2.empty? area2 = area2.get - union = OpenStudio::join(ft_p1, ft_p2, TOL2) return false if union.empty? union = union.get area = OpenStudio::getArea(union) - return empty("union", mth, ERR, a) if area.empty? + return empty("#{i1}:#{i2} union area", mth, ERR, a) if area.empty? area = area.get return false if area < TOL + true + end + + ## + # Callback when other modules extend OSlg + # + # @param base [Object] instance or class object + def self.extended(base) + base.send(:include, self) end end