Class: AedgK12EnvelopeAndEntryInfiltration
- Inherits:
-
OpenStudio::Measure::ModelMeasure
- Object
- OpenStudio::Measure::ModelMeasure
- AedgK12EnvelopeAndEntryInfiltration
- Includes:
- OsLib_AedgMeasures, OsLib_HelperMethods, OsLib_OutdoorAirAndInfiltration, OsLib_Schedules
- Defined in:
- lib/measures/AedgK12EnvelopeAndEntryInfiltration/measure.rb
Overview
start the measure
Instance Method Summary collapse
-
#arguments(model) ⇒ Object
define the arguments that the user will input.
-
#name ⇒ Object
define the name that a user will see, this method may be deprecated as the display name in PAT comes from the name field in measure.xml.
-
#run(model, runner, user_arguments) ⇒ Object
define what happens when the measure is run.
Methods included from OsLib_HelperMethods
checkChoiceArgFromModelObjects, checkDoubleAndIntegerArguments, checkOptionalChoiceArgFromModelObjects, check_upstream_measure_for_arg, createRunVariables, getAreaOfSpacesInArray, getSpaceTypeStandardsInformation, getTotalCostForObjects, log_msgs, neatConvertWithUnitDisplay, populateChoiceArgFromModelObjects, setup_log_msgs
Methods included from OsLib_AedgMeasures
getClimateZoneNumber, getK12Tips, getLongHowToTips, getSmMdOffTips
Instance Method Details
#arguments(model) ⇒ Object
define the arguments that the user will input
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/measures/AedgK12EnvelopeAndEntryInfiltration/measure.rb', line 39 def arguments(model) args = OpenStudio::Measure::OSArgumentVector.new # make choice argument for target performance choices = OpenStudio::StringVector.new choices << 'AEDG K-12 - Baseline' choices << 'AEDG K-12 - Target' infiltrationEnvelope = OpenStudio::Measure::OSArgument.makeChoiceArgument('infiltrationEnvelope', choices) infiltrationEnvelope.setDisplayName('Envelope Infiltration Level (Not including Occupant Entry Infiltration)') infiltrationEnvelope.setDefaultValue('AEDG K-12 - Target') args << infiltrationEnvelope # make choice argument for vestibule preference choices = OpenStudio::StringVector.new choices << 'Model Occupant Entry With a Vestibule if Recommended by K12 AEDG' choices << "Don't model Occupant Entry Infiltration With a Vestibule" choices << 'Model Occupant Entry With a Vestibule' infiltrationOccupant = OpenStudio::Measure::OSArgument.makeChoiceArgument('infiltrationOccupant', choices) infiltrationOccupant.setDisplayName('Occupant Entry Infiltration Modeling Approach') infiltrationOccupant.setDefaultValue('Model Occupant Entry With a Vestibule if Recommended by K12 AEDG') args << infiltrationOccupant # putting stories and names into hash story_args = model.getBuildingStorys story_args_hash = {} story_args.each do |story_arg| next if story_arg.spaces.size <= 0 story_args_hash[story_arg.name.to_s] = story_arg end # call method to make argument handles and display names from hash of model objects storyChoiceArgument = OsLib_HelperMethods.populateChoiceArgFromModelObjects(model, story_args_hash, includeBuilding = nil) # make an argument for construction (todo - it would be nice to make this optional and have infiltration spread across entire building if no stories exist) story = OpenStudio::Measure::OSArgument.makeChoiceArgument('story', storyChoiceArgument['modelObject_handles'], storyChoiceArgument['modelObject_display_names'], true) story.setDisplayName('Apply Occupant Entry Infiltration to ThermalZones on this floor.') if !storyChoiceArgument['modelObject_display_names'][0].nil? story.setDefaultValue(storyChoiceArgument['modelObject_display_names'][0]) end args << story # make an argument for number primary occupant entry points num_entries = OpenStudio::Measure::OSArgument.makeIntegerArgument('num_entries', true) num_entries.setDisplayName('Number of Primary Occupant Entry Points on Selected Floor.') num_entries.setDefaultValue(4) args << num_entries # make an argument for number primary occupant entry points doorOpeningEventsPerPerson = OpenStudio::Measure::OSArgument.makeDoubleArgument('doorOpeningEventsPerPerson', true) doorOpeningEventsPerPerson.setDisplayName('Number of Door Opening Events Per Person Per Day (2 is expected minimum for one entry and exit).') doorOpeningEventsPerPerson.setDefaultValue(3.0) args << doorOpeningEventsPerPerson # make an argument for number primary occupant entry points pressureDifferenceAcrossDoor_pa = OpenStudio::Measure::OSArgument.makeDoubleArgument('pressureDifferenceAcrossDoor_pa', true) pressureDifferenceAcrossDoor_pa.setDisplayName('Pressure Difference Across Door At Occupant Entries (pa).') pressureDifferenceAcrossDoor_pa.setDefaultValue(4.0) args << pressureDifferenceAcrossDoor_pa # make an argument for material and installation cost costTotalEnvelopeInfiltration = OpenStudio::Measure::OSArgument.makeDoubleArgument('costTotalEnvelopeInfiltration', true) costTotalEnvelopeInfiltration.setDisplayName('Total cost for all Envelope Improvements ($).') costTotalEnvelopeInfiltration.setDefaultValue(0.0) args << costTotalEnvelopeInfiltration # make an argument for material and installation cost costTotalEntryInfiltration = OpenStudio::Measure::OSArgument.makeDoubleArgument('costTotalEntryInfiltration', true) costTotalEntryInfiltration.setDisplayName('Total cost for all Occupant Entry Improvements ($).') costTotalEntryInfiltration.setDefaultValue(0.0) args << costTotalEntryInfiltration return args end |
#name ⇒ Object
define the name that a user will see, this method may be deprecated as the display name in PAT comes from the name field in measure.xml
34 35 36 |
# File 'lib/measures/AedgK12EnvelopeAndEntryInfiltration/measure.rb', line 34 def name return 'AedgK12EnvelopeAndEntryInfiltration' end |
#run(model, runner, user_arguments) ⇒ Object
define what happens when the measure is run
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/measures/AedgK12EnvelopeAndEntryInfiltration/measure.rb', line 114 def run(model, runner, user_arguments) super(model, runner, user_arguments) # use the built-in error checking if !runner.validateUserArguments(arguments(model), user_arguments) return false end # assign the user inputs to variables infiltrationEnvelope = runner.getStringArgumentValue('infiltrationEnvelope', user_arguments) infiltrationOccupant = runner.getStringArgumentValue('infiltrationOccupant', user_arguments) story = runner.getOptionalWorkspaceObjectChoiceValue('story', user_arguments, model) # model is passed in because of argument type num_entries = runner.getIntegerArgumentValue('num_entries', user_arguments) doorOpeningEventsPerPerson = runner.getDoubleArgumentValue('doorOpeningEventsPerPerson', user_arguments) pressureDifferenceAcrossDoor_pa = runner.getDoubleArgumentValue('pressureDifferenceAcrossDoor_pa', user_arguments) costTotalEnvelopeInfiltration = runner.getDoubleArgumentValue('costTotalEnvelopeInfiltration', user_arguments) costTotalEntryInfiltration = runner.getDoubleArgumentValue('costTotalEntryInfiltration', user_arguments) # check that story exists in model modelObjectCheck = OsLib_HelperMethods.checkChoiceArgFromModelObjects(story, 'story', 'to_BuildingStory', runner, user_arguments) if modelObjectCheck == false return false else story = modelObjectCheck['modelObject'] apply_to_building = modelObjectCheck['apply_to_building'] end # check arguments for reasonableness checkDoubleArguments = OsLib_HelperMethods.checkDoubleAndIntegerArguments(runner, user_arguments, 'min' => 0.0, 'max' => nil, 'min_eq_bool' => true, 'max_eq_bool' => true, 'arg_array' => ['num_entries', 'doorOpeningEventsPerPerson']) if !checkDoubleArguments then return false end # global variables for costs expected_life = 25 years_until_costs_start = 0 # reporting initial condition of model space_infiltration_objects = model.getSpaceInfiltrationDesignFlowRates if !space_infiltration_objects.empty? runner.registerInitialCondition("The initial model contained #{space_infiltration_objects.size} space infiltration objects.") else runner.registerInitialCondition('The initial model did not contain any space infiltration objects.') end # erase existing infiltration objects used in the model, but save most commonly used schedule # todo - would be nice to preserve attic space infiltration. There are a number of possible solutions for this removedInfiltration = OsLib_OutdoorAirAndInfiltration.eraseInfiltrationUsedInModel(model, runner) # find most common hard assigned from removed infiltration objects if !removedInfiltration.empty? defaultSchedule = removedInfiltration[0][0] # not sure why this is array vs. hash. I wanted to use removedInfiltration.keys[0] else defaultSchedule = nil end # get desired envelope infiltration area if infiltrationEnvelope == 'AEDG K-12 - Baseline' targetFlowPerExteriorArea = 0.0003048 # 0.06 cfm/ft^2 else targetFlowPerExteriorArea = 0.000254 # 0.05 cfm/ft^2 end # hash to pass into infiltration method = { 'nameSuffix' => ' - envelope infiltration', # add this to object name for infiltration 'defaultBuildingSchedule' => defaultSchedule, # this will set schedule set for selected object 'setCalculationMethod' => 'setFlowperExteriorSurfaceArea', 'valueForSelectedCalcMethod' => targetFlowPerExteriorArea } # add in new envelope infiltration to all spaces in the model newInfiltrationPerExteriorSurfaceArea = OsLib_OutdoorAirAndInfiltration.addSpaceInfiltrationDesignFlowRate(model, runner, model.getBuilding, ) targetFlowPerExteriorArea_ip = OpenStudio.convert(targetFlowPerExteriorArea, 'm/s', 'ft/min').get runner.registerInfo("Adding infiltration object to all spaces in model with value of #{OpenStudio.toNeatString(targetFlowPerExteriorArea_ip, 2, true)} (cfm/ft^2) of exterior surface area.") # create lifecycle costs for floors envelopeImprovementTotalCost = 0 totalArea = model.building.get.exteriorSurfaceArea newInfiltrationPerExteriorSurfaceArea.each do |infiltrationObject| spaceType = infiltrationObject.spaceType.get areaForEnvelopeInfiltration_si = OsLib_HelperMethods.getAreaOfSpacesInArray(model, spaceType.spaces, 'exteriorArea')['totalArea'] fractionOfTotal = areaForEnvelopeInfiltration_si / totalArea lcc_mat = OpenStudio::Model::LifeCycleCost.createLifeCycleCost("#{spaceType.name} - Entry Infiltration Cost", model.getBuilding, fractionOfTotal * costTotalEnvelopeInfiltration, 'CostPerEach', 'Construction', expected_life, years_until_costs_start) envelopeImprovementTotalCost += lcc_mat.get.totalCost end # get model climate zone and size and set defaultVestibule flag vestibuleFlag = false # check if vestibule should be used if infiltrationOccupant == "Don't model Occupant Entry Infiltration With a Vestibule" vestibuleFlag = false elsif infiltrationOccupant == 'Model Occupant Entry With a Vestibule' vestibuleFlag = true else climateZoneNumber = OsLib_AedgMeasures.getClimateZoneNumber(model, runner) if climateZoneNumber == false return false elsif climateZoneNumber.to_f > 3 vestibuleFlag = true elsif climateZoneNumber.to_f == 3 building = model.getBuilding if building.floorArea > OpenStudio.convert(10000.0, 'ft^2', 'm^2').get vestibuleFlag = true end end end scheduleWeightHash = {} # make hash of schedules used for occupancy and then the number of people associated with it. Take instance multiplier into account nonRulesetScheduleWeighHash = {} # make hash of schedules used for occupancy and then the number of people associated with it. Take instance multiplier into account peopleInstances = model.getPeoples peopleInstances.each do |peopleInstance| # get value from def # get schedule if !peopleInstance.numberofPeopleSchedule.empty? # get floor area for spaceType or space if !peopleInstance.spaceType.empty? spaceArray = peopleInstance.spaceType.get.spaces else spaceArray = [peopleInstance.space.get] # making an array just so I can pass in what is expected to measure end schedule = peopleInstance.numberofPeopleSchedule.get floorArea = OsLib_HelperMethods.getAreaOfSpacesInArray(model, spaceArray, areaType = 'floorArea')['totalArea'] if !schedule.to_ScheduleRuleset.empty? if scheduleWeightHash[schedule] scheduleWeightHash[schedule] += peopleInstance.getNumberOfPeople(floorArea) else scheduleWeightHash[schedule] = peopleInstance.getNumberOfPeople(floorArea) end else # maybe use hash later to get proper number of people vs. just people related to ruleset schedules if nonRulesetScheduleWeighHash[schedule] nonRulesetScheduleWeighHash[schedule] += peopleInstance.getNumberOfPeople(floorArea) else nonRulesetScheduleWeighHash[schedule] = peopleInstance.getNumberOfPeople(floorArea) end runner.registerWarning("#{peopleInstance.name} uses '#{schedule.name}' as a schedule. It isn't a ScheduleRuleset object. That may affect the results of this measure.") end else runner.registerWarning("#{peopleInstance.name} does not have a schedule associated with it.") end end # get maxPeopleInBuilding with merged occupancy schedule mergedSchedule = OsLib_Schedules.weightedMergeScheduleRulesets(model, scheduleWeightHash) # get max value for merged occupancy schedule maxFractionMergedOccupancy = OsLib_Schedules.getMinMaxAnnualProfileValue(model, mergedSchedule['mergedSchedule']) # create rate of change schedule from merged schedule rateOfChange = OsLib_Schedules.scheduleFromRateOfChange(model, mergedSchedule['mergedSchedule']) # get max value for rate of change. this will help determine max people per hour maxFractionRateOfChange = OsLib_Schedules.getMinMaxAnnualProfileValue(model, rateOfChange) # misc inputs areaPerDoorOpening_ip = 21.0 # ft^2 pressureDifferenceAcrossDoor_wc = pressureDifferenceAcrossDoor_pa / 250 # wc typicalOperationHours = 12.0 # get fraction for merge of occupancy schedule if doorOpeningEventsPerPerson <= 2.0 fractionForRateOfChange = 1.0 else fractionForRateOfChange = (2.0 / doorOpeningEventsPerPerson) * 0.6 # multiplier added to get closer to expected area under curve. end # merge the pre and post rate of change schedules together. mergedRateSchedule = OsLib_Schedules.weightedMergeScheduleRulesets(model, mergedSchedule['mergedSchedule'] => (1.0 - fractionForRateOfChange), rateOfChange => fractionForRateOfChange) mergedRateSchedule['mergedSchedule'].setName('Merged Rate of Change/Occupancy Hybrid') # TODO: - until I can make the merge schedule script work on rules I'm going to hard code rule to with 0 value on weekends and summer runner.registerInfo('Occupant Entry Infiltration schedule based on default rule profile of people schedules. Hard coded to apply monday through friday, September 1st through June 30th.') hybridSchedule = mergedRateSchedule['mergedSchedule'] yearDescription = model.getYearDescription summerStart = yearDescription.makeDate(7, 1) summerEnd = yearDescription.makeDate(8, 31) # create weekend rule weekendRule = OpenStudio::Model::ScheduleRule.new(hybridSchedule) weekendRule.setApplySaturday(true) weekendRule.setApplySunday(true) # create summer rule summerRule = OpenStudio::Model::ScheduleRule.new(hybridSchedule) summerRule.setStartDate(summerStart) summerRule.setEndDate(summerEnd) summerRule.setApplySaturday(true) summerRule.setApplySunday(true) summerRule.setApplyMonday(true) summerRule.setApplyTuesday(true) summerRule.setApplyWednesday(true) summerRule.setApplyThursday(true) summerRule.setApplyFriday(true) # create schedule days to use with summer weekend and summer rules weekendProfile = weekendRule.daySchedule weekendProfile.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0.0) summerProfile = weekendRule.daySchedule summerProfile.addValue(OpenStudio::Time.new(0, 24, 0, 0), 0.0) typicalPeopleInBuilding = mergedSchedule['denominator'] * maxFractionMergedOccupancy['max'] # this is max capacity from people objects * max annual schedule fraction value if num_entries > 0 typicalAvgPeoplePerHour = (typicalPeopleInBuilding * doorOpeningEventsPerPerson) / (typicalOperationHours * num_entries) else typicalAvgPeoplePerHour = 0 end # prepare rule hash for airflow coefficient. Uses people/hour/door as input rules = [] # [people per hour per door, airflow coef with vest, airflow coef without vest] finalPeoplePerHour = nil # this will be used a little later lowAbs = nil # values from ASHRAE Fundamentals 16.26 figure 16 for automatic doors with and without vestibules (people per hour per door, with vestibule, without) rules << [0.0, 0.0, 0.0] rules << [75.0, 190.0, 275.0] rules << [150.0, 315.0, 500.0] rules << [225.0, 475.0, 750.0] rules << [300.0, 610.0, 900.0] rules << [375.0, 750.0, 1100.0] rules << [450.0, 850.0, 1225.0] # make rule hash for cleaner code rulesHash = {} rules.each do |rule| rulesHash[rule[0]] = { 'vestibule' => rule[1], 'noVestibule' => rule[2] } end # get airflow coef from rules vestibuleFlag ? (hashValue = 'vestibule') : (hashValue = 'noVestibule') # get rule above and below target people per hour and interpolate airflow coefficient lower = nil upper = nil target = typicalAvgPeoplePerHour # calculated earlier rulesHash.each do |peoplePerHour, values| if target >= peoplePerHour then lower = peoplePerHour end if target <= peoplePerHour upper = peoplePerHour next end end if lower.nil? then lower = 0 end if upper.nil? then upper = 450.0 end range = upper - lower airflowCoefficient = ((upper - target) / range) * rulesHash[lower][hashValue] + ((target - lower) / range) * rulesHash[upper][hashValue] # Method 2 formula for occupant entry airflow rate from 16.26 of the 2013 ASHRAE Fundamentals airFlowRateCfm = num_entries * airflowCoefficient * areaPerDoorOpening_ip * Math.sqrt(pressureDifferenceAcrossDoor_wc) airFlowRate_si = OpenStudio.convert(airFlowRateCfm, 'ft^3', 'm^3').get / 60 # couldn't direct get CFM to m^3/s runner.registerInfo("Objects representing #{OpenStudio.toNeatString(airFlowRateCfm, 2, true)}(cfm) of infiltration will be added to spaces on #{story.name}. Calculated with an airflow coefficient of #{OpenStudio.toNeatString(airflowCoefficient, 0, true)} for each door. This was calculated on a max door events per hour per door of #{OpenStudio.toNeatString(num_entries, 0, true)}. Occupancy schedules in your model were used to both determine the airflow coefficient and to create a custom schedule to use with this infiltration object.") if vestibuleFlag runner.registerInfo('While infiltration at primary occupant entries is based on using vestibules, vestibule geometry was not added to the model. Per K12 AEDG how to implement recommendation EN18 interior and exterior doors should have a minimum distance between them of not less than 16 ft when in the closed position.') end # find floor area selected floor spaces areaForOccupantEntryInfiltration_si = OsLib_HelperMethods.getAreaOfSpacesInArray(model, story.spaces) # hash to pass into infiltration method = { 'nameSuffix' => ' - occupant entry infiltration', # add this to object name for infiltration 'schedule' => mergedRateSchedule['mergedSchedule'], # this will set schedule set for selected object 'setCalculationMethod' => 'setFlowperSpaceFloorArea', 'valueForSelectedCalcMethod' => airFlowRate_si / areaForOccupantEntryInfiltration_si['totalArea'] } # add in new envelope infiltration to all spaces in the model newInfiltrationPerFloorArea = OsLib_OutdoorAirAndInfiltration.addSpaceInfiltrationDesignFlowRate(model, runner, story.spaces, ) # create lifecycle costs for floors entryImprovementTotalCost = 0 totalArea = areaForOccupantEntryInfiltration_si['totalArea'] storySpaceHash = areaForOccupantEntryInfiltration_si['spaceAreaHash'] newInfiltrationPerFloorArea.each do |infiltrationObject| space = infiltrationObject.space.get fractionOfTotal = storySpaceHash[space] / totalArea lcc_mat = OpenStudio::Model::LifeCycleCost.createLifeCycleCost("#{space.name} - Entry Infiltration Cost", space, fractionOfTotal * costTotalEntryInfiltration, 'CostPerEach', 'Construction', expected_life, years_until_costs_start) entryImprovementTotalCost += lcc_mat.get.totalCost end # populate AEDG tip keys aedgTips = [] # always need tip 17 aedgTips.push('EN17') if vestibuleFlag aedgTips.push('EN18') end # don't really need not applicable flag on this measure, any building with spaces will be affected # populate how to tip messages aedgTipsLong = OsLib_AedgMeasures.getLongHowToTips('K12', aedgTips.uniq.sort, runner) if !aedgTipsLong return false # this should only happen if measure writer passes bad values to getLongHowToTips end # reporting final condition of model space_infiltration_objects = model.getSpaceInfiltrationDesignFlowRates if !space_infiltration_objects.empty? runner.registerFinalCondition("The final model contains #{space_infiltration_objects.size} space infiltration objects. Cost was increased by $#{OpenStudio.toNeatString(envelopeImprovementTotalCost, 2, true)} for envelope infiltration, and $#{OpenStudio.toNeatString(entryImprovementTotalCost, 2, true)} for occupant entry infiltration. #{aedgTipsLong}") else runner.registerFinalCondition("The final model does not contain any space infiltration objects. Cost was increased by $#{OpenStudio.toNeatString(envelopeImprovementTotalCost, 2, true)} for envelope infiltration, and $#{OpenStudio.toNeatString(envelopeImprovementTotalCost, 2, true)} for occupant entry infiltration. #{aedgTipsLong}") end return true end |