openstudio-geb 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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