Class: CreateBarFromModel

Inherits:
OpenStudio::Measure::ModelMeasure
  • Object
show all
Defined in:
lib/measures/create_bar_from_model/measure.rb

Overview

start the measure

Instance Method Summary collapse

Instance Method Details

#arguments(model) ⇒ Object

define the arguments that the user will input



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/measures/create_bar_from_model/measure.rb', line 31

def arguments(model)
  args = OpenStudio::Measure::OSArgumentVector.new

  # make an argument for bar calculation method
  choices = OpenStudio::StringVector.new
  choices << 'Bar - Reduced Bounding Box' # maintains aspect ratio of bounding box and floor area
  choices << 'Bar - Reduced Width' # hybrid of the reduced bounding box and the stretched bars
  choices << 'Bar - Stretched' # maintains total exterior wall area and floor area
  bar_calc_method = OpenStudio::Measure::OSArgument.makeChoiceArgument('bar_calc_method', choices, true)
  bar_calc_method.setDisplayName('Calculation Method to determine Bar Length and Width.')
  bar_calc_method.setDefaultValue('Bar - Reduced Bounding Box')
  args << bar_calc_method

  # make an argument for bar sub-division approach
  choices = OpenStudio::StringVector.new
  choices << 'Single Space Type - Core and Perimeter'
  choices << 'Multiple Space Types - Simple Sliced'
  choices << 'Multiple Space Types - Individual Stories Sliced'
  bar_division_method = OpenStudio::Measure::OSArgument.makeChoiceArgument('bar_division_method', choices, true)
  bar_division_method.setDisplayName('Division Method for Bar Spaces.')
  bar_division_method.setDefaultValue('Single Space Type - Core and Perimeter')
  args << bar_division_method

  return args
end

#descriptionObject

human readable description



21
22
23
# File 'lib/measures/create_bar_from_model/measure.rb', line 21

def description
  return 'Create a core and perimeter bar envelope based on analysis of initial model geometry.'
end

#modeler_descriptionObject

human readable description of modeling approach



26
27
28
# File 'lib/measures/create_bar_from_model/measure.rb', line 26

def modeler_description
  return 'Gather orientation and story specific construction, fenestration (including overhang) specific information'
end

#nameObject

human readable name



16
17
18
# File 'lib/measures/create_bar_from_model/measure.rb', line 16

def name
  return 'Create Bar From Model'
end

#run(model, runner, user_arguments) ⇒ Object

define what happens when the measure is run



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/measures/create_bar_from_model/measure.rb', line 58

def run(model, runner, user_arguments)
  super(model, runner, user_arguments)

  # use the built-in error checking
  if !runner.validateUserArguments(arguments(model), user_arguments)
    return false
  end

  bar_calc_method = runner.getStringArgumentValue('bar_calc_method', user_arguments)
  bar_division_method = runner.getStringArgumentValue('bar_division_method', user_arguments)

  # TODO: - in future may investigate best rotation to fit rectangle
  # todo - in future will investigate all constructions to inform new envelope For now will rely on building default construction set
  # todo - in future store HVAC system type by zone with floor area for each system (identify what is primary)
  # todo - in future store information on exhaust fans

  # TODO: - space type blending measure should be run upstream if necessary, but could warn user if all spaces of original model don't have space space type assignments
  # todo - warn user of any space loads that will be lost with envelope (I think thi sis addressed)
  # todo - warn user about daylighing control objects that will be removed. In future could add new similar controls back into model

  # assign the user inputs to variables

  # report initial condition of model
  runner.registerInitialCondition("The building started with #{model.getSpaces.size} spaces.")

  # gather_envelope_data
  envelope_data_hash = OpenstudioStandards::Geometry.model_envelope_data(model)

  # report summary of initial geometry
  runner.registerValue('rotation', envelope_data_hash[:north_axis], 'degrees')
  runner.registerInfo("Initial building rotation is #{envelope_data_hash[:north_axis]} degrees.")

  runner.registerValue('building_floor_area', envelope_data_hash[:building_floor_area], 'm^2')
  building_floor_area_ip = OpenStudio.convert(envelope_data_hash[:building_floor_area], 'm^2', 'ft^2').get
  runner.registerInfo("Initial building floor area is #{OpenStudio.toNeatString(building_floor_area_ip, 0, true)} (ft^2)")

  runner.registerValue('wwr_n', envelope_data_hash[:building_wwr_n], 'ratio')
  runner.registerValue('wwr_s', envelope_data_hash[:building_wwr_s], 'ratio')
  runner.registerValue('wwr_e', envelope_data_hash[:building_wwr_e], 'ratio')
  runner.registerValue('wwr_w', envelope_data_hash[:building_wwr_w], 'ratio')
  runner.registerInfo("Initial building North WWR is #{envelope_data_hash[:building_wwr_n].round(2)}.")
  runner.registerInfo("Initial building South WWR is #{envelope_data_hash[:building_wwr_s].round(2)}.")
  runner.registerInfo("Initial building East WWR is #{envelope_data_hash[:building_wwr_e].round(2)}.")
  runner.registerInfo("Initial building West WWR is #{envelope_data_hash[:building_wwr_w].round(2)}.")

  runner.registerValue('proj_factor_n', envelope_data_hash[:building_overhang_proj_factor_n], 'ratio')
  if envelope_data_hash[:building_overhang_proj_factor_n] > 0
    runner.registerInfo("Initial building North projection factor is #{envelope_data_hash[:building_overhang_proj_factor_n].round(2)}.")
  end
  runner.registerValue('proj_factor_s', envelope_data_hash[:building_overhang_proj_factor_n], 'ratio')
  if envelope_data_hash[:building_overhang_proj_factor_s] > 0
    runner.registerInfo("Initial building South projection factor is #{envelope_data_hash[:building_overhang_proj_factor_s].round(2)}.")
  end
  runner.registerValue('proj_factor_e', envelope_data_hash[:building_overhang_proj_factor_n], 'ratio')
  if envelope_data_hash[:building_overhang_proj_factor_e] > 0
    runner.registerInfo("Initial building East projection factor is #{envelope_data_hash[:building_overhang_proj_factor_e].round(2)}.")
  end
  runner.registerValue('proj_factor_w', envelope_data_hash[:building_overhang_proj_factor_n], 'ratio')
  if envelope_data_hash[:building_overhang_proj_factor_w] > 0
    runner.registerInfo("Initial building West projection factor is #{envelope_data_hash[:building_overhang_proj_factor_w].round(2)}.")
  end

  runner.registerValue('min_x', envelope_data_hash[:building_min_xyz][0], 'm')
  runner.registerValue('min_y', envelope_data_hash[:building_min_xyz][1], 'm')
  runner.registerValue('min_z', envelope_data_hash[:building_min_xyz][2], 'm')
  runner.registerValue('max_x', envelope_data_hash[:building_max_xyz][0], 'm')
  runner.registerValue('max_y', envelope_data_hash[:building_max_xyz][1], 'm')
  runner.registerValue('max_z', envelope_data_hash[:building_max_xyz][2], 'm')
  min_x_ip = OpenStudio.convert(envelope_data_hash[:building_min_xyz][0], 'm', 'ft').get.round(2)
  min_y_ip = OpenStudio.convert(envelope_data_hash[:building_min_xyz][1], 'm', 'ft').get.round(2)
  min_z_ip = OpenStudio.convert(envelope_data_hash[:building_min_xyz][2], 'm', 'ft').get.round(2)
  max_x_ip = OpenStudio.convert(envelope_data_hash[:building_max_xyz][0], 'm', 'ft').get.round(2)
  max_y_ip = OpenStudio.convert(envelope_data_hash[:building_max_xyz][1], 'm', 'ft').get.round(2)
  max_z_ip = OpenStudio.convert(envelope_data_hash[:building_max_xyz][2], 'm', 'ft').get.round(2)
  effective_number_of_stories_above_grade = 0 # will populate this when looping through stories
  effective_number_of_stories_below_grade = 0 # will populate this when looping through stories
  runner.registerInfo("Intial bounding box is [#{min_x_ip},#{min_y_ip},#{min_z_ip}] and [#{max_x_ip},#{max_y_ip},#{max_z_ip}] (ft).")

  # TODO: - pass in story and space type hashes as runner.registerValues?

  envelope_data_hash[:stories].each do |story, hash|
    min_height_ip = OpenStudio.convert(hash[:story_min_height], 'm', 'ft').get
    max_height_ip = OpenStudio.convert(hash[:story_max_height], 'm', 'ft').get
    story_footprint = OpenStudio.convert(hash[:story_footprint], 'm^2', 'ft^2').get
    story_perimeter = OpenStudio.convert(hash[:story_perimeter], 'm', 'ft').get
    story_string = []
    story_string << "#{story.name} geometry ranges from #{min_height_ip.round(2)} (ft) #{max_height_ip.round(2)} (ft)."
    story_string << "#{story.name} has a footprint if #{OpenStudio.toNeatString(story_footprint, 0, true)} (ft^2) and an exterior perimeter of #{OpenStudio.toNeatString(story_perimeter, 0, true)} (ft)."
    if !(envelope_data_hash[:stories][story][:story_included_in_building_area])
      story_string << " * #{story.name} has one or more spaces not included in the building area, it may represent a plenum or attic. It should not contribute to the story count for the building"
    else
      # populate effective number of above and below grade stories
      if !envelope_data_hash[:stories][story][:story_has_ground_walls].empty?
        story_string << " * #{story.name} appears to represent a below grade building story."
        effective_number_of_stories_below_grade += envelope_data_hash[:stories][story][:story_min_multiplier]
      else
        effective_number_of_stories_above_grade += envelope_data_hash[:stories][story][:story_min_multiplier]
      end
    end
    if envelope_data_hash[:stories][story][:story_min_multiplier] > 1
      story_string << " * #{story.name} appears to represent #{envelope_data_hash[:stories][story][:story_min_multiplier]} building stories."
    end
    if !envelope_data_hash[:stories][story][:story_has_adiabatic_walls].empty?
      story_string << " * One or more spaces on #{story.name} have surfaces with adiabatic boundary condtions."

      if !envelope_data_hash[:stories][story][:story_party_walls].empty?
        if envelope_data_hash[:stories][story][:story_party_walls].include?('north')
          runner.registerInfo(" * One or more walls on the North side of #{story.name} appear to represent party walls.")
        end
        if envelope_data_hash[:stories][story][:story_party_walls].include?('south')
          runner.registerInfo(" * One or more walls on the South side of #{story.name} appear to represent party walls.")
        end
        if envelope_data_hash[:stories][story][:story_party_walls].include?('east')
          runner.registerInfo(" * One or more walls on the East side of #{story.name} appear to represent party walls.")
        end
        if envelope_data_hash[:stories][story][:story_party_walls].include?('west')
          runner.registerInfo(" * One or more walls on the West side of #{story.name} appear to represent party walls.")
        end
      end

    end
    story_string.each do |string|
      runner.registerInfo(string)
    end
  end

  # log effective number of stories in hash
  envelope_data_hash[:effective_num_stories_below_grade] = effective_number_of_stories_below_grade
  envelope_data_hash[:effective_num_stories_above_grade] = effective_number_of_stories_above_grade
  envelope_data_hash[:effective__num_stories] = effective_number_of_stories_below_grade + effective_number_of_stories_above_grade
  envelope_data_hash[:floor_height] = envelope_data_hash[:building_max_xyz][2] / envelope_data_hash[:effective__num_stories].to_f
  runner.registerInfo("The building has #{effective_number_of_stories_below_grade} below grade stories and #{effective_number_of_stories_above_grade} above grade stories.")

  # TODO: - issue with calculated perimeter methods, estimate whole building perimeter instead
  building_perimeter_estimated = envelope_data_hash[:building_exterior_wall_area] / (effective_number_of_stories_above_grade * envelope_data_hash[:floor_height])
  runner.registerValue('building_perimeter', building_perimeter_estimated, 'm')
  building_perimeter_ip = OpenStudio.convert(building_perimeter_estimated, 'm', 'ft').get
  runner.registerInfo("Initial building average perimeter is #{OpenStudio.toNeatString(building_perimeter_ip, 0, true)} (ft).")
  # runner.registerValue('building_perimeter',envelope_data_hash[:building_floor_area],'m')
  # building_perimeter_ip = OpenStudio.convert(envelope_data_hash[:building_perimeter], 'm', 'ft').get
  # runner.registerInfo("Initial building ground floor perimeter is #{OpenStudio.toNeatString(building_perimeter_ip,0,true)} (ft).")

  # report space type breakdown
  total_area_with_space_types = 0
  space_type_ratios = {}
  envelope_data_hash[:space_types].each do |space_type, hash|
    total_area_with_space_types += hash[:floor_area]
  end
  # loop through stories and report ratio and thermostat information
  envelope_data_hash[:space_types].each do |space_type, hash|
    space_type_ratio = hash[:floor_area] / total_area_with_space_types
    space_type_ratios[space_type] = space_type_ratio
  end
  space_type_ratios = space_type_ratios.sort_by { |k, v| v }.reverse
  space_type_ratios.each do |space_type, ratio|
    runner.registerInfo("#{ratio.round(3)} - Ratio of building floor area that is #{space_type.name}")
  end

  # report on thermostats
  htg_setpoint_ratios = {} # key is setpoint value is ratio
  clg_setpoint_ratios = {} # key is setpoint value is ratio
  htg_setpoints = {} # key is space type value is schedule
  clg_setpoints = {} # key is space type value is schedule
  space_type_ratios.each do |space_type, ratio|
    target_htg_setpoint_schedule = envelope_data_hash[:space_types][space_type][:htg_setpoint].key(envelope_data_hash[:space_types][space_type][:htg_setpoint].values.max)
    target_clg_setpoint_schedule = envelope_data_hash[:space_types][space_type][:clg_setpoint].key(envelope_data_hash[:space_types][space_type][:clg_setpoint].values.max)
    htg_setpoints[space_type] = target_htg_setpoint_schedule
    clg_setpoints[space_type] = target_clg_setpoint_schedule

    # skip if space type doesn't have heating and cooling thermostats
    if !(target_htg_setpoint_schedule.nil? && target_clg_setpoint_schedule.nil?)

      runner.registerInfo("Setpoint schedules for #{space_type.name} are #{target_htg_setpoint_schedule.name} for heating and #{target_clg_setpoint_schedule.name} for cooling.")
      if envelope_data_hash[:space_types][space_type][:htg_setpoint].size > 1
        runner.registerInfo(" * More than one heating setpoint schedule was used for zones with #{space_type.name}. Listed schedule was used over the largest floor area for this space type.")
      end
      if envelope_data_hash[:space_types][space_type][:clg_setpoint].size > 1
        runner.registerInfo(" * More than one cooling setpoint schedule was used for zones with #{space_type.name}. Listed schedule was used over the largest floor area for this space type.")
      end

      # update htg_setpoint_ratios and clg_setpoint_ratios
      if htg_setpoint_ratios.key?(target_htg_setpoint_schedule)
        htg_setpoint_ratios[target_htg_setpoint_schedule] += envelope_data_hash[:space_types][space_type][:htg_setpoint].values.max
      else
        htg_setpoint_ratios[target_htg_setpoint_schedule] = envelope_data_hash[:space_types][space_type][:htg_setpoint].values.max
      end
      if clg_setpoint_ratios.key?(target_clg_setpoint_schedule)
        clg_setpoint_ratios[target_clg_setpoint_schedule] += envelope_data_hash[:space_types][space_type][:clg_setpoint].values.max
      else
        clg_setpoint_ratios[target_clg_setpoint_schedule] = envelope_data_hash[:space_types][space_type][:clg_setpoint].values.max
      end

    else
      runner.registerInfo("Didn't find or assign heating and cooling thermostat for #{space_type.name}")
    end
  end

  # left these in for diagnostics if I want to see full contents of hashes
  #  puts envelope_data_hash
  #  envelope_data_hash[:space_types].each do |k,v|
  #   puts k.name
  #   puts v
  #  end
  #  envelope_data_hash[:stories].each do |k,v|
  #    puts v
  #  end

  # define length and with of bar
  case bar_calc_method
  when 'Bar - Reduced Bounding Box'
    bar_calc = OpenstudioStandards::Geometry.bar_reduced_bounding_box(envelope_data_hash)
  when 'Bar - Reduced Width'
    bar_calc = OpenstudioStandards::Geometry.bar_reduced_width(envelope_data_hash)
  when 'Bar - Stretched'
    bar_calc = OpenstudioStandards::Geometry.bar_stretched(envelope_data_hash)
  end

  # populate bar_hash and create envelope with data from envelope_data_hash and user arguments
  bar_hash = {}
  bar_hash[:length] = bar_calc[:length]
  bar_hash[:width] = bar_calc[:width]
  bar_hash[:building_perimeter] = envelope_data_hash[:building_perimeter] # just using ground floor perimeter
  bar_hash[:num_stories] = envelope_data_hash[:effective__num_stories]
  bar_hash[:num_stories_below_grade] = envelope_data_hash[:effective_num_stories_below_grade]
  bar_hash[:num_stories_above_grade] = envelope_data_hash[:effective_num_stories_above_grade]
  bar_hash[:floor_height] = envelope_data_hash[:floor_height]
  center_x = (envelope_data_hash[:building_max_xyz][0] + envelope_data_hash[:building_min_xyz][0]) / 2.0
  center_y = (envelope_data_hash[:building_max_xyz][1] + envelope_data_hash[:building_min_xyz][1]) / 2.0
  center_z = envelope_data_hash[:building_min_xyz][2]
  bar_hash[:center_of_footprint] = OpenStudio::Point3d.new(center_x, center_y, center_z)
  bar_hash[:bar_division_method] = bar_division_method
  bar_hash[:space_types] = envelope_data_hash[:space_types]
  bar_hash[:building_wwr_n] = envelope_data_hash[:building_wwr_n]
  bar_hash[:building_wwr_s] = envelope_data_hash[:building_wwr_s]
  bar_hash[:building_wwr_e] = envelope_data_hash[:building_wwr_e]
  bar_hash[:building_wwr_w] = envelope_data_hash[:building_wwr_w]
  bar_hash[:stories] = envelope_data_hash[:stories]

  # remove exhaust from zones to re-apply to new zone after create_bar (for now not keeping zone mixing or zone ventilation design flow rate)
  # when using create_typical_model with this measure choose None for exhaust makeup air so don't have any dummy exhaust objects
  model.getFanZoneExhausts.each(&:removeFromThermalZone)

  # remove non-resource objects (this wasn't added to standards which explains some other measure test failures)
  # remove_non_resource_objects(runner, model)

  # create bar
  OpenstudioStandards::Geometry.create_bar(model, bar_hash)

  # move exhaust from temp zone to large zone in new model
  zone_hash = {} # key is zone value is floor area. It excludes zones with non 1 multiplier
  model.getThermalZones.each do |thermal_zone|
    next if thermal_zone.multiplier > 1

    zone_hash[thermal_zone] = thermal_zone.floorArea
  end
  target_zone = zone_hash.key(zone_hash.values.max)
  model.getFanZoneExhausts.each do |exhaust|
    exhaust.addToThermalZone(target_zone)
  end

  # assign thermostats
  if !htg_setpoint_ratios.empty? || !clg_setpoint_ratios.empty?
    if bar_division_method.include?('Single Space Type')

      mode_target_htg_setpoint_sch = htg_setpoint_ratios.key(htg_setpoint_ratios.values.max)
      mode_target_clg_setpoint_sch = clg_setpoint_ratios.key(clg_setpoint_ratios.values.max)
      new_thermostat = OpenStudio::Model::ThermostatSetpointDualSetpoint.new(model)
      new_thermostat.setHeatingSetpointTemperatureSchedule(mode_target_htg_setpoint_sch)
      new_thermostat.setCoolingSetpointTemperatureSchedule(mode_target_clg_setpoint_sch)
      runner.registerInfo("Assigning #{mode_target_htg_setpoint_sch.name} as heating setpoint schedule for all thermal zones.")
      runner.registerInfo("Assigning #{mode_target_clg_setpoint_sch.name} as cooling setpoint schedule for all thermal zones.")
      model.getThermalZones.each do |thermal_zone|
        thermal_zone.setThermostatSetpointDualSetpoint(new_thermostat)
      end

    else

      # restore thermostats for space type saved from old geometry
      model.getThermalZones.each do |thermal_zone|
        
        # todo - figure out why this is happening some times
        if thermal_zone.spaces.size == 0
          runner.registerInfo("Not altering thermostat because thermal zone #{thermal_zone.name} doesn't have any spaces.")
          next
        end
        next if !thermal_zone.spaces.first.spaceType.is_initialized

        space_type = thermal_zone.spaces.first.spaceType.get
        next if htg_setpoints[space_type].nil?
        next if clg_setpoints[space_type].nil?
        new_thermostat = OpenStudio::Model::ThermostatSetpointDualSetpoint.new(model)
        new_thermostat.setHeatingSetpointTemperatureSchedule(htg_setpoints[space_type])
        new_thermostat.setCoolingSetpointTemperatureSchedule(clg_setpoints[space_type])
        thermal_zone.setThermostatSetpointDualSetpoint(new_thermostat)
      end

    end

  end

  # report final ratios
  final_floor_area = model.getBuilding.floorArea
  final_ratios = {}
  model.getSpaceTypes.each do |space_type|
    next if space_type.floorArea == 0.0

    final_ratios[space_type] = space_type.floorArea / final_floor_area
  end
  Hash[final_ratios.sort_by { |k, v| v }.reverse].each do |k, v|
    runner.registerInfo("#{v.round(3)} - Final Ratio for #{k.name}.")
  end

  # report final condition of model
  final_floor_area_ip = OpenStudio.convert(model.getBuilding.floorArea, 'm^2', 'ft^2').get
  runner.registerFinalCondition("The building finished with #{model.getSpaces.size} spaces and a floor area of #{OpenStudio.toNeatString(final_floor_area_ip, 0, true)}.")

  return true
end