openstudio-geb 0.3.2 → 0.3.3

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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/lib/measures/GEB Metrics Report/resources/os_lib_reporting.rb +5 -24
  3. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/LICENSE.md +13 -0
  4. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/README.md +152 -0
  5. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/README.md.erb +45 -0
  6. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/docs/.gitkeep +0 -0
  7. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/measure.rb +604 -0
  8. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/measure.xml +265 -0
  9. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/tests/USA_NY_Buffalo.Niagara.Intl.AP.725280_TMY3.epw +8768 -0
  10. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/tests/add_fan_assist_night_ventilation_with_hybrid_control_test.rb +94 -0
  11. data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/tests/medium_office_with_internal_windows.osm +13459 -0
  12. data/lib/measures/add_heat_pump_water_heater/measure.rb +2 -2
  13. data/lib/measures/apply_dynamic_coating_to_roof_wall/LICENSE.md +1 -0
  14. data/lib/measures/apply_dynamic_coating_to_roof_wall/README.md +101 -0
  15. data/lib/measures/apply_dynamic_coating_to_roof_wall/README.md.erb +45 -0
  16. data/lib/measures/apply_dynamic_coating_to_roof_wall/docs/.gitkeep +0 -0
  17. data/lib/measures/apply_dynamic_coating_to_roof_wall/measure.rb +421 -0
  18. data/lib/measures/apply_dynamic_coating_to_roof_wall/measure.xml +204 -0
  19. data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/MediumOffice-90.1-2010-ASHRAE 169-2013-5A.osm +13669 -0
  20. data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/SF-CACZ6-HPWH-pre1978.osm +16130 -0
  21. data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/USA_NY_Buffalo.Niagara.Intl.AP.725280_TMY3.epw +8768 -0
  22. data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/apply_dynamic_coating_to_roof_wall_test.rb +81 -0
  23. data/lib/openstudio/geb/run.rb +34 -0
  24. data/lib/openstudio/geb/version.rb +1 -1
  25. metadata +23 -3
@@ -0,0 +1,604 @@
1
+ # insert your copyright here
2
+
3
+ # see the URL below for information on how to write OpenStudio measures
4
+ # http://nrel.github.io/OpenStudio-user-documentation/reference/measure_writing_guide/
5
+
6
+ # start the measure
7
+ class AddFanAssistNightVentilationWithHybridControl < OpenStudio::Measure::ModelMeasure
8
+ require 'time'
9
+ require 'json'
10
+
11
+ # human readable name
12
+ def name
13
+ # Measure name should be the title case of the class name.
14
+ return 'Add fan assist night ventilation with hybrid control'
15
+ end
16
+
17
+ # human readable description
18
+ def description
19
+ return 'This measure is modified based on the OS measure "fan_assist_night_ventilation" from "openstudio-ee-gem". ' \
20
+ 'It adds night ventilation that is enabled by opening windows assisted by exhaust fans. Hybrid ventilation ' \
21
+ 'control is added to avoid simultaneous operation of windows and HVAC.'
22
+ end
23
+
24
+ # human readable description of modeling approach
25
+ def modeler_description
26
+ return "This measure adds a zone ventilation object to each zone with operable windwos. The measure will first " \
27
+ "look for a celing opening to find a connection for zone a zone mixing object. If a ceiling isn't found, " \
28
+ "then it looks for a wall. The end result is zone ventilation object followed by a path of zone mixing objects. " \
29
+ "The exhaust fan consumption is modeled in the zone ventilation object, but no heat is brought in from the fan. \n " \
30
+ "Different from the original 'fan_assist_night_ventilation' measure, this measure can be applied to models " \
31
+ "with mechenical systems. HybridVentilationAvailabilityManager is added to airloops and zonal systems to avoid " \
32
+ "simultaneous operation of windows and HVAC. The zone ventilation is controlled by a combination of schedule, " \
33
+ "indoor and outdoor temperature, and wind speed."
34
+ end
35
+
36
+ # define the arguments that the user will input
37
+ def arguments(model)
38
+ args = OpenStudio::Measure::OSArgumentVector.new
39
+
40
+ # make an argument for night ventilation air change rate
41
+ design_night_vent_ach = OpenStudio::Measure::OSArgument::makeDoubleArgument('design_night_vent_ach', true)
42
+ design_night_vent_ach.setDisplayName('Design night ventilation air change rate defined by ACH-air changes per hour')
43
+ design_night_vent_ach.setDefaultValue(3)
44
+ args << design_night_vent_ach
45
+
46
+ # add argument for exhaust fan pressure rise
47
+ fan_pressure_rise = OpenStudio::Measure::OSArgument.makeDoubleArgument('fan_pressure_rise', true)
48
+ fan_pressure_rise.setDisplayName('Fan Pressure Rise')
49
+ fan_pressure_rise.setUnits('Pa')
50
+ fan_pressure_rise.setDefaultValue(500.0)
51
+ args << fan_pressure_rise
52
+
53
+ # add argument for exhaust fan efficiency
54
+ efficiency = OpenStudio::Measure::OSArgument.makeDoubleArgument('efficiency', true)
55
+ efficiency.setDisplayName('Fan Total Efficiency')
56
+ efficiency.setDefaultValue(0.65)
57
+ args << efficiency
58
+
59
+ # make an argument for min indoor temp
60
+ min_indoor_temp = OpenStudio::Measure::OSArgument::makeDoubleArgument('min_indoor_temp', false)
61
+ min_indoor_temp.setDisplayName('Minimum Indoor Temperature (degC)')
62
+ min_indoor_temp.setDescription('The indoor temperature below which ventilation is shutoff.')
63
+ min_indoor_temp.setDefaultValue(20)
64
+ args << min_indoor_temp
65
+
66
+ # make an argument for maximum indoor temperature
67
+ max_indoor_temp = OpenStudio::Measure::OSArgument::makeDoubleArgument('max_indoor_temp', false)
68
+ max_indoor_temp.setDisplayName('Maximum Indoor Temperature (degC)')
69
+ max_indoor_temp.setDescription('The indoor temperature above which ventilation is shutoff.')
70
+ max_indoor_temp.setDefaultValue(26)
71
+ args << max_indoor_temp
72
+
73
+ # make an argument for delta temperature
74
+ delta_temp = OpenStudio::Measure::OSArgument::makeDoubleArgument('delta_temp', false)
75
+ delta_temp.setDisplayName('Minimum Indoor-Outdoor Temperature Difference (degC)')
76
+ delta_temp.setDescription('This is the temperature difference between the indoor and outdoor air dry-bulb '\
77
+ 'temperatures below which ventilation is shutoff. For example, a delta temperature '\
78
+ 'of 2 degC means ventilation is available if the outside air temperature is at least '\
79
+ '2 degC cooler than the zone air temperature. Values can be negative.')
80
+ delta_temp.setDefaultValue(2.0)
81
+ args << delta_temp
82
+
83
+ # make an argument for minimum outdoor temperature
84
+ min_outdoor_temp = OpenStudio::Measure::OSArgument::makeDoubleArgument('min_outdoor_temp', true)
85
+ min_outdoor_temp.setDisplayName('Minimum Outdoor Temperature (degC)')
86
+ min_outdoor_temp.setDescription('The outdoor temperature below which ventilation is shut off.')
87
+ min_outdoor_temp.setDefaultValue(18)
88
+ args << min_outdoor_temp
89
+
90
+ # make an argument for maximum outdoor temperature
91
+ max_outdoor_temp = OpenStudio::Measure::OSArgument::makeDoubleArgument('max_outdoor_temp', true)
92
+ max_outdoor_temp.setDisplayName('Maximum Outdoor Temperature (degC)')
93
+ max_outdoor_temp.setDescription('The outdoor temperature above which ventilation is shut off.')
94
+ max_outdoor_temp.setDefaultValue(26)
95
+ args << max_outdoor_temp
96
+
97
+ # make an argument for maximum wind speed
98
+ max_wind_speed = OpenStudio::Measure::OSArgument::makeDoubleArgument('max_wind_speed', false)
99
+ max_wind_speed.setDisplayName('Maximum Wind Speed (m/s)')
100
+ max_wind_speed.setDescription('This is the wind speed above which ventilation is shut off. The default values assume windows are closed when wind is above a gentle breeze to avoid blowing around papers in the space.')
101
+ max_wind_speed.setDefaultValue(40)
102
+ args << max_wind_speed
103
+
104
+ # make an argument for the start time of natural ventilation
105
+ night_vent_starttime = OpenStudio::Measure::OSArgument.makeStringArgument('night_vent_starttime', false)
106
+ night_vent_starttime.setDisplayName('Daily Start Time for natural ventilation')
107
+ night_vent_starttime.setDescription('Use 24 hour format (HR:MM)')
108
+ night_vent_starttime.setDefaultValue('20:00')
109
+ args << night_vent_starttime
110
+
111
+ # make an argument for the end time of natural ventilation
112
+ night_vent_endtime = OpenStudio::Measure::OSArgument.makeStringArgument('night_vent_endtime', false)
113
+ night_vent_endtime.setDisplayName('Daily End Time for natural ventilation')
114
+ night_vent_endtime.setDescription('Use 24 hour format (HR:MM)')
115
+ night_vent_endtime.setDefaultValue('08:00')
116
+ args << night_vent_endtime
117
+
118
+ # make an argument for the start date of natural ventilation
119
+ night_vent_startdate = OpenStudio::Measure::OSArgument.makeStringArgument('night_vent_startdate', false)
120
+ night_vent_startdate.setDisplayName('Start Date for natural ventilation')
121
+ night_vent_startdate.setDescription('In MM-DD format')
122
+ night_vent_startdate.setDefaultValue('03-01')
123
+ args << night_vent_startdate
124
+
125
+ # make an argument for the end date of natural ventilation
126
+ night_vent_enddate = OpenStudio::Measure::OSArgument.makeStringArgument('night_vent_enddate', false)
127
+ night_vent_enddate.setDisplayName('End Date for natural ventilation')
128
+ night_vent_enddate.setDescription('In MM-DD format')
129
+ night_vent_enddate.setDefaultValue('10-31')
130
+ args << night_vent_enddate
131
+
132
+ # Make boolean arguments for natural ventilation schedule on weekends
133
+ wknds = OpenStudio::Measure::OSArgument.makeBoolArgument('wknds', false)
134
+ wknds.setDisplayName('Allow night time ventilation on weekends')
135
+ wknds.setDefaultValue(true)
136
+ args << wknds
137
+
138
+ return args
139
+ end
140
+
141
+ # define what happens when the measure is run
142
+ def run(model, runner, user_arguments)
143
+ super(model, runner, user_arguments)
144
+
145
+ # use the built-in error checking
146
+ if !runner.validateUserArguments(arguments(model), user_arguments)
147
+ return false
148
+ end
149
+
150
+ # assign the user inputs to variables
151
+ design_night_vent_ach = runner.getDoubleArgumentValue('design_night_vent_ach', user_arguments)
152
+ fan_pressure_rise = runner.getDoubleArgumentValue('fan_pressure_rise', user_arguments)
153
+ efficiency = runner.getDoubleArgumentValue('efficiency', user_arguments)
154
+ min_indoor_temp = runner.getDoubleArgumentValue('min_indoor_temp', user_arguments)
155
+ max_indoor_temp = runner.getDoubleArgumentValue('max_indoor_temp', user_arguments)
156
+ delta_temp = runner.getDoubleArgumentValue('delta_temp', user_arguments)
157
+ min_outdoor_temp = runner.getDoubleArgumentValue('min_outdoor_temp', user_arguments)
158
+ max_outdoor_temp = runner.getDoubleArgumentValue('max_outdoor_temp', user_arguments)
159
+ max_wind_speed = runner.getDoubleArgumentValue('max_wind_speed', user_arguments)
160
+ night_vent_starttime = runner.getStringArgumentValue('night_vent_starttime', user_arguments)
161
+ night_vent_endtime = runner.getStringArgumentValue('night_vent_endtime', user_arguments)
162
+ night_vent_startdate = runner.getStringArgumentValue('night_vent_startdate', user_arguments)
163
+ night_vent_enddate = runner.getStringArgumentValue('night_vent_enddate', user_arguments)
164
+ wknds = runner.getBoolArgumentValue('wknds', user_arguments)
165
+
166
+
167
+ # check night ventilation ach input
168
+ if design_night_vent_ach <= 0
169
+ runner.registerError('Design night ventilation ACH should be positive. Please double check your input.')
170
+ return false
171
+ elsif design_night_vent_ach > 10
172
+ runner.registerWarning("Design night ventilation ACH #{design_night_vent_ach} is higher than 10, which is unusually large. Please double check your input.")
173
+ elsif design_night_vent_ach < 0.3
174
+ runner.registerWarning("Design night ventilation ACH #{design_night_vent_ach} is lower than 0.3, which is unusually small. Please double check your input.")
175
+ end
176
+
177
+ # check fan efficiency input
178
+ if efficiency <= 0
179
+ runner.registerError('Exhaust fan efficiency should be positive. Please double check your input.')
180
+ return false
181
+ elsif efficiency > 1
182
+ runner.registerError("Exhaust fan efficiency #{efficiency} is larger than 1. Exhaust fan efficiency should be between 0 and 1. Please double check your input.")
183
+ return false
184
+ end
185
+
186
+ # check temp limit inputs
187
+ if min_indoor_temp >= max_indoor_temp
188
+ runner.registerError('Minimum indoor temperature should be lower than maximum outdoor temperature. Please double check your input.')
189
+ return false
190
+ end
191
+
192
+ if min_outdoor_temp >= max_outdoor_temp
193
+ runner.registerError('Minimum outdoor temperature should be lower than maximum outdoor temperature. Please double check your input.')
194
+ return false
195
+ end
196
+
197
+ # check max wind speed input
198
+ if max_wind_speed <= 0
199
+ runner.registerError('Maximum wind speed should be positive. Please double check your input.')
200
+ return false
201
+ end
202
+
203
+ # check delta temperature input
204
+ if delta_temp < 0
205
+ runner.registerWarning("delta_temp #{delta_temp} is negative. Normally delta temperature should be positive "\
206
+ "or at least 0 to enable free cooling and avoid introducing extra cooling load. "\
207
+ "Please double check your input.")
208
+ end
209
+
210
+ # check time format
211
+ begin
212
+ night_vent_starttime = Time.strptime(night_vent_starttime, '%H:%M')
213
+ night_vent_endtime = Time.strptime(night_vent_endtime, '%H:%M')
214
+ rescue ArgumentError
215
+ runner.registerError('Natural ventilation start and end time are required, and should be in format of %H:%M, e.g., 16:00.')
216
+ return false
217
+ end
218
+ if night_vent_starttime < night_vent_endtime
219
+ runner.registerWarning('Night ventilation end time is later than start time, referring to non-overnight ' \
220
+ 'natural ventilation. Make sure this is expected.')
221
+ end
222
+
223
+ # check date format
224
+ md = /(\d\d)-(\d\d)/.match(night_vent_startdate)
225
+ if md
226
+ night_vent_start_month = md[1].to_i
227
+ night_vent_start_day = md[2].to_i
228
+ else
229
+ runner.registerError('Start date must be in MM-DD format.')
230
+ return false
231
+ end
232
+
233
+ md = /(\d\d)-(\d\d)/.match(night_vent_enddate)
234
+ if md
235
+ night_vent_end_month = md[1].to_i
236
+ night_vent_end_day = md[2].to_i
237
+ else
238
+ runner.registerError('End date must be in MM-DD format.')
239
+ return false
240
+ end
241
+
242
+ night_vent_startdate_os = model.getYearDescription.makeDate(night_vent_start_month, night_vent_start_day)
243
+ night_vent_enddate_os = model.getYearDescription.makeDate(night_vent_end_month, night_vent_end_day)
244
+
245
+ # if_overnight: 1 or 0; wknds (if applicable to weekends): 1 or 0
246
+ # by default schedule on value is 1, sometimes need specify, e.g., natural ventilation window open area fraction
247
+ def create_sch(model, sch_name, start_time, end_time, start_date, end_date, wknds, sch_on_value=1, sch_off_value=0)
248
+ day_start_time = Time.strptime("00:00", '%H:%M')
249
+ # create discharging schedule
250
+ new_sch_ruleset = OpenStudio::Model::ScheduleRuleset.new(model)
251
+ new_sch_ruleset.setName(sch_name)
252
+ new_sch_ruleset.defaultDaySchedule.setName(sch_name + ' default')
253
+ if start_time > end_time
254
+ if_overnight = sch_on_value # assigned as schedule on value
255
+ else
256
+ if_overnight = sch_off_value # assigned as schedule off value
257
+ end
258
+
259
+ for min in 1..24*60
260
+ if ((end_time - day_start_time)/60).to_i == min
261
+ time = OpenStudio::Time.new(0, 0, min)
262
+ new_sch_ruleset.defaultDaySchedule.addValue(time, sch_on_value)
263
+ elsif ((start_time - day_start_time)/60).to_i == min
264
+ time = OpenStudio::Time.new(0, 0, min)
265
+ new_sch_ruleset.defaultDaySchedule.addValue(time, sch_off_value)
266
+ elsif min == 24*60
267
+ time = OpenStudio::Time.new(0, 0, min)
268
+ new_sch_ruleset.defaultDaySchedule.addValue(time, if_overnight)
269
+ end
270
+ end
271
+
272
+ start_month = start_date.monthOfYear.value
273
+ start_day = start_date.dayOfMonth
274
+ end_month = end_date.monthOfYear.value
275
+ end_day = end_date.dayOfMonth
276
+ ts_rule = OpenStudio::Model::ScheduleRule.new(new_sch_ruleset, new_sch_ruleset.defaultDaySchedule)
277
+ ts_rule.setName("#{new_sch_ruleset.name} #{start_month}/#{start_day}-#{end_month}/#{end_day} Rule")
278
+ ts_rule.setStartDate(start_date)
279
+ ts_rule.setEndDate(end_date)
280
+ ts_rule.setApplyWeekdays(true)
281
+ if wknds
282
+ ts_rule.setApplyWeekends(true)
283
+ else
284
+ ts_rule.setApplyWeekends(false)
285
+ end
286
+
287
+ unless start_month == 1 && start_day == 1
288
+ new_rule_day = OpenStudio::Model::ScheduleDay.new(model)
289
+ new_rule_day.addValue(OpenStudio::Time.new(0,24), 0)
290
+ new_rule = OpenStudio::Model::ScheduleRule.new(new_sch_ruleset, new_rule_day)
291
+ new_rule.setName("#{new_sch_ruleset.name} 01/01-#{start_month}/#{start_day} Rule")
292
+ new_rule.setStartDate(model.getYearDescription.makeDate(1, 1))
293
+ new_rule.setEndDate(model.getYearDescription.makeDate(start_month, start_day))
294
+ new_rule.setApplyAllDays(true)
295
+ end
296
+
297
+ unless end_month == 12 && end_day == 31
298
+ new_rule_day = OpenStudio::Model::ScheduleDay.new(model)
299
+ new_rule_day.addValue(OpenStudio::Time.new(0,24), 0)
300
+ new_rule = OpenStudio::Model::ScheduleRule.new(new_sch_ruleset, new_rule_day)
301
+ new_rule.setName("#{new_sch_ruleset.name} #{end_month}/#{end_day}-12/31 Rule")
302
+ new_rule.setStartDate(model.getYearDescription.makeDate(end_month, end_day))
303
+ new_rule.setEndDate(model.getYearDescription.makeDate(12, 31))
304
+ new_rule.setApplyAllDays(true)
305
+ end
306
+
307
+ return new_sch_ruleset
308
+ end
309
+
310
+ def inspect_airflow_surfaces(zone)
311
+ array = [] # [adjacent_zone,surfaceType]
312
+ zone.spaces.each do |space|
313
+ space.surfaces.each do |surface|
314
+ next if surface.adjacentSurface.is_initialized != true
315
+ next if !surface.adjacentSurface.get.space.is_initialized
316
+ next if !surface.adjacentSurface.get.space.get.thermalZone.is_initialized
317
+ adjacent_zone = surface.adjacentSurface.get.space.get.thermalZone.get
318
+ if surface.surfaceType == 'RoofCeiling' || surface.surfaceType == 'Wall'
319
+ if surface.isAirWall
320
+ array << [adjacent_zone, surface.surfaceType]
321
+ else
322
+ surface.subSurfaces.each do |sub_surface|
323
+ next if sub_surface.adjacentSubSurface.is_initialized != true
324
+ next if !sub_surface.adjacentSubSurface.get.surface.get.space.is_initialized
325
+ next if !sub_surface.adjacentSubSurface.get.surface.get.space.get.thermalZone.is_initialized
326
+ adjacent_zone = sub_surface.adjacentSubSurface.get.surface.get.space.get.thermalZone.get
327
+ # available subsurfacetypes: "FixedWindow", "OperableWindow", "Door", "GlassDoor", "OverheadDoor", "Skylight", "TubularDaylightDome", "TubularDaylightDiffuser"
328
+ # Often windows are assigned as FixedWindow by default but not indicating it cannot be opened.
329
+ if sub_surface.isAirWall || sub_surface.subSurfaceType == 'OperableWindow' || sub_surface.subSurfaceType == 'FixedWindow' ||
330
+ sub_surface.subSurfaceType == 'Door' || sub_surface.subSurfaceType == 'GlassDoor'
331
+ array << [adjacent_zone, surface.surfaceType]
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
338
+
339
+ return array
340
+ end
341
+
342
+ # report initial condition of model
343
+ runner.registerInitialCondition("The building started with #{model.getZoneVentilationDesignFlowRates.size} "\
344
+ " zone ventilation design flow rate objects and #{model.getZoneMixings.size} zone mixing objects.")
345
+
346
+
347
+ #################### STEP 1: find ventilation path and create exhaust zones ################
348
+ # setup hash to hold path objects and exhaust zones
349
+ path_objects = {}
350
+ exhaust_zones = {}
351
+ zones_w_ext_windows = []
352
+ nv_zone_list = {} # save zones with exterior windows and their zone ventilation object info
353
+
354
+ model.getSpaces.each do |space|
355
+ next if space.thermalZone.empty?
356
+ thermal_zone = space.thermalZone.get
357
+
358
+ # store airflow paths for future use
359
+ path_objects[thermal_zone] = inspect_airflow_surfaces(thermal_zone)
360
+
361
+ # get the list of zones with exterior windows
362
+ space.surfaces.sort.each do |surface|
363
+ surface.subSurfaces.sort.each do |subsurface|
364
+ if (subsurface.subSurfaceType == 'OperableWindow' || subsurface.subSurfaceType == 'FixedWindow') && subsurface.outsideBoundaryCondition == 'Outdoors'
365
+ zones_w_ext_windows << thermal_zone unless zones_w_ext_windows.include?(thermal_zone)
366
+ end
367
+ end
368
+ end
369
+ end
370
+
371
+ #################### STEP 2: add zone ventilation objects and zone mixing objects ################
372
+ # setup has to store paths
373
+ flow_paths = {}
374
+ # setup night ventilation schedule
375
+ night_vent_sch = create_sch(model, "night ventilation sch", night_vent_starttime, night_vent_endtime, night_vent_startdate_os, night_vent_enddate_os, wknds)
376
+
377
+ # return as NA if no exterior operable windows
378
+ if zones_w_ext_windows.empty?
379
+ runner.registerAsNotApplicable('No zones with exterior operable windows were found. The model will not be altered')
380
+ return true
381
+ end
382
+
383
+ # Loop through zones in hash and make natural ventilation objects so the sum equals the user specified target
384
+ zones_w_ext_windows.each do |zone|
385
+ zone_ventilation = OpenStudio::Model::ZoneVentilationDesignFlowRate.new(model)
386
+ zone_ventilation.setName("PathStart_#{zone.name}")
387
+ zone_ventilation.addToThermalZone(zone)
388
+ zone_ventilation.setVentilationType('Exhaust') # switched from Natural to use power. Need to set fan properties. Used exhaust so no heat from fan in stream
389
+ zone_ventilation.setAirChangesperHour(design_night_vent_ach)
390
+
391
+ # inputs used for fan power
392
+ zone_ventilation.setFanPressureRise(fan_pressure_rise)
393
+ zone_ventilation.setFanTotalEfficiency(efficiency)
394
+
395
+ # set schedule from user arg
396
+ zone_ventilation.setSchedule(night_vent_sch)
397
+
398
+ # set ventilation control thresholds
399
+ zone_ventilation.setMinimumIndoorTemperature(min_indoor_temp)
400
+ zone_ventilation.setMaximumIndoorTemperature(max_indoor_temp)
401
+ zone_ventilation.setMinimumOutdoorTemperature(min_outdoor_temp)
402
+ zone_ventilation.setMaximumOutdoorTemperature(max_outdoor_temp)
403
+ zone_ventilation.setDeltaTemperature(delta_temp)
404
+ zone_ventilation.setMaximumWindSpeed(max_wind_speed)
405
+ nv_zone_list[zone.name.to_s] = zone_ventilation
406
+ runner.registerInfo("Added natural ventilation object to #{zone.name} of #{design_night_vent_ach} air change rate per hour.")
407
+
408
+ # start trace of path adding air mixing objects
409
+ found_path_end = false
410
+ flow_paths[zone] = []
411
+ current_zone = zone
412
+ zones_used_for_this_path = [current_zone]
413
+ until found_path_end
414
+ found_ceiling = false
415
+ found_wall = false
416
+ path_objects[current_zone].each do |object|
417
+ next if zones_used_for_this_path.include?(object[0])
418
+ next if object[1].to_s != 'RoofCeiling'
419
+ next if zones_w_ext_windows.include?(object[0])
420
+ if found_ceiling
421
+ runner.registerWarning("Found more than one possible airflow path for #{current_zone.name}")
422
+ else
423
+ flow_paths[zone] << object[0]
424
+ current_zone = object[0]
425
+ zones_used_for_this_path << object[0]
426
+ found_ceiling = true
427
+ end
428
+ end
429
+ unless found_ceiling
430
+ path_objects[current_zone].each do |object|
431
+ next if zones_used_for_this_path.include?(object[0])
432
+ next if object[1].to_s != 'Wall'
433
+ next if zones_w_ext_windows.include?(object[0])
434
+ if found_wall
435
+ runner.registerWarning("Found more than one possible airflow path for #{current_zone.name}")
436
+ else
437
+ flow_paths[zone] << object[0]
438
+ current_zone = object[0]
439
+ zones_used_for_this_path << object[0]
440
+ found_wall = true
441
+ end
442
+ end
443
+ end
444
+ if !found_ceiling && !found_wall
445
+ found_path_end = true
446
+ end
447
+ end
448
+
449
+ # add one way air mixing objects along path zones
450
+ zone_path_string_array = [zone.name]
451
+ vent_zone = zone
452
+ source_zone = zone
453
+ flow_paths[zone].each do |zone|
454
+ zone_mixing = OpenStudio::Model::ZoneMixing.new(zone)
455
+ zone_mixing.setName("PathStart_#{vent_zone.name}_#{source_zone.name}")
456
+ zone_mixing.setSourceZone(source_zone)
457
+ zone_mixing.setAirChangesperHour(design_night_vent_ach)
458
+
459
+ # set min outdoor temp schedule
460
+ min_outdoor_sch = OpenStudio::Model::ScheduleConstant.new(model)
461
+ min_outdoor_sch.setValue(min_outdoor_temp)
462
+ zone_mixing.setMinimumOutdoorTemperatureSchedule(min_outdoor_sch)
463
+
464
+ # set schedule from user arg
465
+ zone_mixing.setSchedule(night_vent_sch)
466
+
467
+ # change source zone to what was just target zone
468
+ zone_path_string_array << zone.name
469
+ source_zone = zone
470
+ end
471
+ runner.registerInfo("Added Zone Mixing Path: #{zone_path_string_array.join(' > ')}")
472
+
473
+ # add ach to exhaust zones
474
+ if !flow_paths[zone].empty?
475
+ if exhaust_zones.include? flow_paths[zone].last
476
+ exhaust_zones[flow_paths[zone].last] += design_night_vent_ach
477
+ else
478
+ exhaust_zones[flow_paths[zone].last] = design_night_vent_ach
479
+ end
480
+ else
481
+ # extra code if there is no path from entry zone
482
+ if exhaust_zones.include? zone
483
+ exhaust_zones[zone] += design_night_vent_ach
484
+ else
485
+ exhaust_zones[zone] = design_night_vent_ach
486
+ runner.registerWarning("#{zone.name} doesn't have path to other zones. Exhaust assumed to be with the same zone as air enters.")
487
+ end
488
+ end
489
+ end
490
+
491
+ # report how much air (by ach) exhausts to each exhaust zone
492
+ # when I add an exhaust fan to the top floor I want it to use energy but I don't want to move any additional air.
493
+ # The air is already being brought into the zone by the zone mixing objects
494
+ exhaust_zones.each do |zone, ach|
495
+ runner.registerInfo("Zone Mixing flow rate into #{zone.name} is #{ach} air change per hour. Fan Consumption included with zone ventilation zones.")
496
+
497
+ # check for exterior surface area
498
+ if zone.exteriorSurfaceArea == 0
499
+ runner.registerWarning("Exhaust Zone #{zone.name} doesn't appear to have any exterior exposure. Review the paths to see that this is the expected result.")
500
+ end
501
+ end
502
+
503
+ # warn if zone multiplier are used
504
+ non_default_multiplier = []
505
+ model.getThermalZones.each do |zone|
506
+ if zone.multiplier > 1
507
+ non_default_multiplier << zone
508
+ end
509
+ end
510
+ if !non_default_multiplier.empty?
511
+ runner.registerWarning("This measure is not intended to be use when thermal zones have a non 1 multiplier. #{non_default_multiplier.size} zones in this model have multipliers greater than one. Results are likley invalid.")
512
+ end
513
+
514
+
515
+ #################### STEP 3: add hybrid ventilation control objects ################
516
+
517
+ # TODO: Simple Airflow Control Type Schedule Name set as 1 denote group control
518
+ # if a zone has more than one windows, group control allows them to be operated simultaneously
519
+ # if an airloopHVAC controls more than one zone, only one AvailabilityManagerHybridVentilation is allowed for an airloop, group control will
520
+ # decides the zones controlled by this airloop based on the selected ZoneVentilation object input
521
+ vent_control_sch = create_sch(model, "ventilation control sch", night_vent_starttime, night_vent_endtime, night_vent_startdate_os, night_vent_enddate_os, wknds, 1, 0)
522
+ simple_airflow_control_type_sch = OpenStudio::Model::ScheduleConstant.new(model)
523
+ simple_airflow_control_type_sch.setName("simple airflow control type sch - group control")
524
+ simple_airflow_control_type_sch.setValue(1)
525
+
526
+ # part1: loop through all the airloopHVAC and add hybrid ventilation control
527
+ model.getAirLoopHVACs.sort.each do |air_loop|
528
+ max_zone_area = 0
529
+ nv_zone_with_max_area = nil
530
+ air_loop.thermalZones.each do |thermal_zone|
531
+ if max_zone_area < thermal_zone.floorArea && nv_zone_list.key?(thermal_zone.name.to_s)
532
+ max_zone_area = thermal_zone.floorArea
533
+ nv_zone_with_max_area = thermal_zone.name.to_s
534
+ end
535
+ end
536
+ next if nv_zone_with_max_area.nil? # if there is no NV zone in this airloop, skip
537
+ has_hybrid_avail_manager = false
538
+ air_loop.availabilityManagers.sort.each do |avail_mgr|
539
+ next if avail_mgr.to_AvailabilityManagerHybridVentilation.empty?
540
+ if avail_mgr.to_AvailabilityManagerHybridVentilation.is_initialized
541
+ has_hybrid_avail_manager = true
542
+ avail_mgr_hybr_vent = avail_mgr.to_AvailabilityManagerHybridVentilation.get
543
+ avail_mgr_hybr_vent.setVentilationControlModeSchedule(vent_control_sch)
544
+ avail_mgr_hybr_vent.setControlledZone(model.getThermalZoneByName(nv_zone_with_max_area).get)
545
+ avail_mgr_hybr_vent.setMinimumOutdoorTemperature(min_outdoor_temp)
546
+ avail_mgr_hybr_vent.setMaximumOutdoorTemperature(max_outdoor_temp)
547
+ avail_mgr_hybr_vent.setMaximumWindSpeed(max_wind_speed)
548
+ avail_mgr_hybr_vent.setSimpleAirflowControlTypeSchedule(simple_airflow_control_type_sch)
549
+ avail_mgr_hybr_vent.setZoneVentilationObject(nv_zone_list[nv_zone_with_max_area])
550
+ end
551
+ end
552
+
553
+ unless has_hybrid_avail_manager
554
+ avail_mgr_hybr_vent = OpenStudio::Model::AvailabilityManagerHybridVentilation.new(model)
555
+ avail_mgr_hybr_vent.setName(air_loop.name.to_s + " HybridVentilation AvailabilityManager")
556
+ avail_mgr_hybr_vent.setVentilationControlModeSchedule(vent_control_sch)
557
+ avail_mgr_hybr_vent.setControlledZone(model.getThermalZoneByName(nv_zone_with_max_area).get)
558
+ avail_mgr_hybr_vent.setMinimumOutdoorTemperature(min_outdoor_temp)
559
+ avail_mgr_hybr_vent.setMaximumOutdoorTemperature(max_outdoor_temp)
560
+ avail_mgr_hybr_vent.setMaximumWindSpeed(max_wind_speed)
561
+ avail_mgr_hybr_vent.setSimpleAirflowControlTypeSchedule(simple_airflow_control_type_sch)
562
+ avail_mgr_hybr_vent.setZoneVentilationObject(nv_zone_list[nv_zone_with_max_area])
563
+ air_loop.addAvailabilityManager(avail_mgr_hybr_vent)
564
+ end
565
+
566
+ # remove thermal zones in this airloop from the nv_zone_list hash
567
+ air_loop.thermalZones.each do |thermal_zone|
568
+ if nv_zone_list.key?(thermal_zone.name.to_s)
569
+ nv_zone_list.delete(thermal_zone.name.to_s)
570
+ end
571
+ end
572
+ end
573
+
574
+ # part2: loop through all spaces, add hybrid vent control to zones that are not connected to airloopHVAC but uses zonal equipment
575
+ nv_zone_list.each do |zone_name, nv_obj|
576
+ avail_mgr_hybr_vent = OpenStudio::Model::AvailabilityManagerHybridVentilation.new(model)
577
+ avail_mgr_hybr_vent.setName(zone_name + " HybridVentilation AvailabilityManager")
578
+ avail_mgr_hybr_vent.setVentilationControlModeSchedule(vent_control_sch)
579
+ avail_mgr_hybr_vent.setControlledZone(model.getThermalZoneByName(zone_name).get)
580
+ avail_mgr_hybr_vent.setMinimumOutdoorTemperature(min_outdoor_temp)
581
+ avail_mgr_hybr_vent.setMaximumOutdoorTemperature(max_outdoor_temp)
582
+ avail_mgr_hybr_vent.setMaximumWindSpeed(max_wind_speed)
583
+ avail_mgr_hybr_vent.setSimpleAirflowControlTypeSchedule(simple_airflow_control_type_sch)
584
+ avail_mgr_hybr_vent.setZoneVentilationObject(nv_obj)
585
+ end
586
+
587
+ # echo added AvailabilityManagerHybridVentilation object number to the user
588
+ runner.registerInfo("A total of #{model.getAvailabilityManagerHybridVentilations.size} AvailabilityManagerHybridVentilations were added.")
589
+
590
+ # report final condition of model
591
+ runner.registerFinalCondition("The building finished with #{model.getZoneVentilationDesignFlowRates.size} zone ventilation design flow rate objects and #{model.getZoneMixings.size} zone mixing objects.")
592
+
593
+ # adding useful output variables for diagnostics
594
+ OpenStudio::Model::OutputVariable.new('Zone Mixing Current Density Air Volume Flow Rate', model)
595
+ OpenStudio::Model::OutputVariable.new('Zone Ventilation Current Density Volume Flow Rate', model)
596
+ OpenStudio::Model::OutputVariable.new('Zone Ventilation Fan Electric Energy', model)
597
+ OpenStudio::Model::OutputVariable.new('Zone Outdoor Air Drybulb Temperature', model)
598
+
599
+ return true
600
+ end
601
+ end
602
+
603
+ # register the measure to be used by the application
604
+ AddFanAssistNightVentilationWithHybridControl.new.registerWithApplication