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.
- checksums.yaml +4 -4
- data/lib/measures/GEB Metrics Report/resources/os_lib_reporting.rb +5 -24
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/LICENSE.md +13 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/README.md +152 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/README.md.erb +45 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/docs/.gitkeep +0 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/measure.rb +604 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/measure.xml +265 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/tests/USA_NY_Buffalo.Niagara.Intl.AP.725280_TMY3.epw +8768 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/tests/add_fan_assist_night_ventilation_with_hybrid_control_test.rb +94 -0
- data/lib/measures/add_fan_assist_night_ventilation_with_hybrid_control/tests/medium_office_with_internal_windows.osm +13459 -0
- data/lib/measures/add_heat_pump_water_heater/measure.rb +2 -2
- data/lib/measures/apply_dynamic_coating_to_roof_wall/LICENSE.md +1 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/README.md +101 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/README.md.erb +45 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/docs/.gitkeep +0 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/measure.rb +421 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/measure.xml +204 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/MediumOffice-90.1-2010-ASHRAE 169-2013-5A.osm +13669 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/SF-CACZ6-HPWH-pre1978.osm +16130 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/USA_NY_Buffalo.Niagara.Intl.AP.725280_TMY3.epw +8768 -0
- data/lib/measures/apply_dynamic_coating_to_roof_wall/tests/apply_dynamic_coating_to_roof_wall_test.rb +81 -0
- data/lib/openstudio/geb/run.rb +34 -0
- data/lib/openstudio/geb/version.rb +1 -1
- 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
|