buildingsync 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/continuous_integration.yml +146 -0
  3. data/.gitignore +33 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +10 -0
  6. data/CHANGELOG.md +50 -0
  7. data/Gemfile +31 -0
  8. data/Jenkinsfile +10 -0
  9. data/LICENSE.md +29 -0
  10. data/README.md +105 -0
  11. data/Rakefile +77 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/buildingsync.gemspec +37 -0
  15. data/config.rb.in +26 -0
  16. data/doc_templates/LICENSE.md +29 -0
  17. data/doc_templates/README.md.erb +42 -0
  18. data/doc_templates/copyright_erb.txt +38 -0
  19. data/doc_templates/copyright_js.txt +5 -0
  20. data/doc_templates/copyright_ruby.txt +36 -0
  21. data/lib/buildingsync.rb +43 -0
  22. data/lib/buildingsync/all_resource_total.rb +54 -0
  23. data/lib/buildingsync/audit_date.rb +54 -0
  24. data/lib/buildingsync/constants.rb +49 -0
  25. data/lib/buildingsync/contact.rb +54 -0
  26. data/lib/buildingsync/extension.rb +57 -0
  27. data/lib/buildingsync/generator.rb +584 -0
  28. data/lib/buildingsync/get_bcl_weather_file.rb +326 -0
  29. data/lib/buildingsync/helpers/Model.hvac.rb +216 -0
  30. data/lib/buildingsync/helpers/helper.rb +494 -0
  31. data/lib/buildingsync/helpers/xml_get_set.rb +215 -0
  32. data/lib/buildingsync/makers/phase_zero_base.osw +178 -0
  33. data/lib/buildingsync/makers/workflow_maker.json +811 -0
  34. data/lib/buildingsync/makers/workflow_maker.rb +581 -0
  35. data/lib/buildingsync/makers/workflow_maker_base.rb +167 -0
  36. data/lib/buildingsync/model_articulation/building.rb +1119 -0
  37. data/lib/buildingsync/model_articulation/building_and_system_types.json +121 -0
  38. data/lib/buildingsync/model_articulation/building_section.rb +190 -0
  39. data/lib/buildingsync/model_articulation/building_system.rb +49 -0
  40. data/lib/buildingsync/model_articulation/envelope_system.rb +102 -0
  41. data/lib/buildingsync/model_articulation/exterior_floor_system_type.rb +64 -0
  42. data/lib/buildingsync/model_articulation/facility.rb +439 -0
  43. data/lib/buildingsync/model_articulation/foundation_system_type.rb +64 -0
  44. data/lib/buildingsync/model_articulation/hvac_system.rb +395 -0
  45. data/lib/buildingsync/model_articulation/lighting_system.rb +102 -0
  46. data/lib/buildingsync/model_articulation/loads_system.rb +287 -0
  47. data/lib/buildingsync/model_articulation/location_element.rb +129 -0
  48. data/lib/buildingsync/model_articulation/measure.rb +57 -0
  49. data/lib/buildingsync/model_articulation/roof_system_type.rb +64 -0
  50. data/lib/buildingsync/model_articulation/service_hot_water_system.rb +87 -0
  51. data/lib/buildingsync/model_articulation/site.rb +242 -0
  52. data/lib/buildingsync/model_articulation/spatial_element.rb +343 -0
  53. data/lib/buildingsync/model_articulation/wall_system_type.rb +64 -0
  54. data/lib/buildingsync/report.rb +217 -0
  55. data/lib/buildingsync/resource_use.rb +55 -0
  56. data/lib/buildingsync/scenario.rb +622 -0
  57. data/lib/buildingsync/selection_tool.rb +98 -0
  58. data/lib/buildingsync/time_series.rb +85 -0
  59. data/lib/buildingsync/translator.rb +167 -0
  60. data/lib/buildingsync/utility.rb +67 -0
  61. data/lib/buildingsync/version.rb +45 -0
  62. metadata +223 -0
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ # *******************************************************************************
4
+ # OpenStudio(R), Copyright (c) 2008-2020, Alliance for Sustainable Energy, LLC.
5
+ # BuildingSync(R), Copyright (c) 2015-2020, Alliance for Sustainable Energy, LLC.
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # (1) Redistributions of source code must retain the above copyright notice,
12
+ # this list of conditions and the following disclaimer.
13
+ #
14
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # (3) Neither the name of the copyright holder nor the names of any contributors
19
+ # may be used to endorse or promote products derived from this software without
20
+ # specific prior written permission from the respective party.
21
+ #
22
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
23
+ # of modifications or other derivative works may not use the "OpenStudio"
24
+ # trademark, "OS", "os", or any other confusingly similar designation without
25
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
26
+ #
27
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
28
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
29
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
31
+ # UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
32
+ # THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
33
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
34
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
35
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
36
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
37
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
+ # *******************************************************************************
39
+
40
+ require 'fileutils'
41
+ require 'json'
42
+
43
+ module BuildingSync
44
+ # base class for objects that will configure workflows based on building sync files
45
+ class WorkflowMakerBase
46
+ # initialize
47
+ # @param doc [REXML::Document]
48
+ # @param ns [String]
49
+ def initialize(doc, ns)
50
+ if !doc.is_a?(REXML::Document)
51
+ raise StandardError, "doc must be an REXML::Document. Passed object of class: #{doc.class}"
52
+ end
53
+
54
+ if !ns.is_a?(String)
55
+ raise StandardError, "ns must be String. Passed object of class: #{ns.class}"
56
+ end
57
+
58
+ @doc = doc
59
+ @ns = ns
60
+ @workflow = nil
61
+ end
62
+
63
+ def get_prefix
64
+ if @ns == ''
65
+ return ''
66
+ else
67
+ return "#{@ns}:"
68
+ end
69
+ end
70
+
71
+ # TODO: add a schema validation and re-ordering mechanism for XML elements
72
+ # Format, add declaration, and write xml to disk
73
+ # @param filename [String] full path including filename, i.e. output/path/results.xml
74
+ def save_xml(filename)
75
+ # first we make sure all directories exist
76
+ FileUtils.mkdir_p(File.dirname(filename))
77
+
78
+ # Setup formatting
79
+ formatter = REXML::Formatters::Pretty.new
80
+ formatter.compact = true
81
+
82
+ # Setup document declaration
83
+ decl = REXML::XMLDecl.new
84
+ decl.encoding = REXML::XMLDecl::DEFAULT_ENCODING # UTF-8
85
+ @doc << decl
86
+
87
+ # Write file
88
+ File.open(filename, 'w') do |file|
89
+ formatter.write(@doc, file)
90
+ end
91
+ end
92
+
93
+ # set only one measure path
94
+ # @param workflow [Hash] a hash of the openstudio workflow
95
+ # @param measures_dir [String]
96
+ def set_measure_path(workflow, measures_dir)
97
+ workflow['measure_paths'] = [measures_dir]
98
+ end
99
+
100
+ # set multiple measure paths
101
+ # @param measures_dir_array [Array]
102
+ def set_measure_paths(measures_dir_array)
103
+ @workflow['measure_paths'] = measures_dir_array
104
+ end
105
+
106
+ # clear all measures from the list in the workflow
107
+ def clear_all_measures
108
+ @workflow.delete('steps')
109
+ @workflow['steps'] = []
110
+ end
111
+
112
+ # add measure path
113
+ # @param measures_dir [String]
114
+ # @return [Boolean]
115
+ def add_measure_path(measures_dir)
116
+ @workflow['measure_paths'].each do |dir|
117
+ if dir == measures_dir
118
+ return false
119
+ end
120
+ end
121
+ @workflow['measure_paths'] << measures_dir
122
+ return true
123
+ end
124
+
125
+ # set measure argument
126
+ # @param workflow [Hash] a hash of the openstudio workflow
127
+ # @param measure_dir_name [String] the directory name for the measure, as it appears
128
+ # in any of the gems, i.e. openstudio-common-measures-gem/lib/measures/[measure_dir_name]
129
+ # @param argument_name [String]
130
+ # @param argument_value [String]
131
+ # @return [Boolean]
132
+ def set_measure_argument(workflow, measure_dir_name, argument_name, argument_value)
133
+ result = false
134
+ workflow['steps'].each do |step|
135
+ if step['measure_dir_name'] == measure_dir_name
136
+ step['arguments'][argument_name] = argument_value
137
+ result = true
138
+ end
139
+ end
140
+
141
+ if !result
142
+ raise "Could not set '#{argument_name}' to '#{argument_value}' for measure '#{measure_dir_name}'"
143
+ end
144
+
145
+ return result
146
+ end
147
+
148
+ # Adds a new measure to the workflow ONLY if it doesn't already exist
149
+ # @param workflow [Hash] a hash of the openstudio workflow
150
+ # @param measure_dir_name [String] the directory name for the measure, as it appears
151
+ # in any of the gems, i.e. openstudio-common-measures-gem/lib/measures/[measure_dir_name]
152
+ # @return [Boolean] whether or not a new measure was added
153
+ def add_new_measure(workflow, measure_dir_name)
154
+ # first we check if the measure already exists
155
+ workflow['steps'].each do |step|
156
+ if step['measure_dir_name'] == measure_dir_name
157
+ return false
158
+ end
159
+ end
160
+ # if it does not exist we add it
161
+ new_step = {}
162
+ new_step['measure_dir_name'] = measure_dir_name
163
+ workflow['steps'].unshift(new_step)
164
+ return true
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,1119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # *******************************************************************************
4
+ # OpenStudio(R), Copyright (c) 2008-2020, Alliance for Sustainable Energy, LLC.
5
+ # BuildingSync(R), Copyright (c) 2015-2020, Alliance for Sustainable Energy, LLC.
6
+ # All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions are met:
10
+ #
11
+ # (1) Redistributions of source code must retain the above copyright notice,
12
+ # this list of conditions and the following disclaimer.
13
+ #
14
+ # (2) Redistributions in binary form must reproduce the above copyright notice,
15
+ # this list of conditions and the following disclaimer in the documentation
16
+ # and/or other materials provided with the distribution.
17
+ #
18
+ # (3) Neither the name of the copyright holder nor the names of any contributors
19
+ # may be used to endorse or promote products derived from this software without
20
+ # specific prior written permission from the respective party.
21
+ #
22
+ # (4) Other than as required in clauses (1) and (2), distributions in any form
23
+ # of modifications or other derivative works may not use the "OpenStudio"
24
+ # trademark, "OS", "os", or any other confusingly similar designation without
25
+ # specific prior written permission from Alliance for Sustainable Energy, LLC.
26
+ #
27
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
28
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO
29
+ # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
31
+ # UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
32
+ # THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
33
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
34
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
35
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
36
+ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
37
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
+ # *******************************************************************************
39
+ require 'date'
40
+
41
+ require 'openstudio/extension/core/os_lib_helper_methods'
42
+ require 'openstudio/extension/core/os_lib_model_generation'
43
+
44
+ require 'buildingsync/model_articulation/building_section'
45
+ require 'buildingsync/model_articulation/location_element'
46
+ require 'buildingsync/get_bcl_weather_file'
47
+
48
+ module BuildingSync
49
+ # Building class
50
+ class Building < LocationElement
51
+ include OsLib_HelperMethods
52
+ include EnergyPlus
53
+ include OsLib_ModelGeneration
54
+
55
+ # initialize
56
+ # @param building_element [REXML::Element] an element corresponding to a single auc:Building
57
+ # @param site_occupancy_classification [String]
58
+ # @param site_total_floor_area [String]
59
+ # @param ns [String] namespace, likely 'auc'
60
+ def initialize(base_xml, site_occupancy_classification, site_total_floor_area, ns)
61
+ super(base_xml, ns)
62
+ @base_xml = base_xml
63
+ @ns = ns
64
+
65
+ help_element_class_type_check(base_xml, 'Building')
66
+ @building_sections = []
67
+ @building_sections_whole_building = []
68
+ @model = nil
69
+ @all_set = false
70
+
71
+ # parameter to read and write.
72
+ @epw_file_path = nil
73
+ @standard_template = nil
74
+ @building_rotation = 0.0
75
+ @floor_height = 0.0
76
+ @width = 0.0
77
+ @length = 0.0
78
+ @wwr = 0.0
79
+ @name = nil
80
+ # variables not used during read xml for now
81
+ @party_wall_stories_north = 0
82
+ @party_wall_stories_south = 0
83
+ @party_wall_stories_west = 0
84
+ @party_wall_stories_east = 0
85
+ @party_wall_fraction = 0
86
+ @built_year = 0
87
+ @open_studio_standard = nil
88
+ @occupant_quantity = nil
89
+ @number_of_units = nil
90
+ @fraction_area = 1.0
91
+ # code to initialize
92
+ read_xml(site_occupancy_classification, site_total_floor_area)
93
+ end
94
+
95
+ # returns number of stories
96
+ # @return [Integer]
97
+ def num_stories
98
+ return @num_stories_above_grade + @num_stories_below_grade
99
+ end
100
+
101
+ # read xml
102
+ # @param site_occupancy_classification [String]
103
+ # @param site_total_floor_area [String]
104
+ def read_xml(site_occupancy_classification, site_total_floor_area)
105
+ # floor areas
106
+ @total_floor_area = read_floor_areas(site_total_floor_area)
107
+ # read location specific values
108
+ read_location_values
109
+ check_occupancy_classification(site_occupancy_classification)
110
+ set_built_year
111
+
112
+ # deal with stories above and below grade
113
+ read_stories_above_and_below_grade
114
+ # aspect ratio
115
+ set_ns_to_ew_ratio
116
+
117
+ # Create the BuildingSections
118
+ @base_xml.elements.each("#{@ns}:Sections/#{@ns}:Section") do |section_element|
119
+ section = BuildingSection.new(section_element, xget_text('OccupancyClassification'), @total_floor_area, num_stories, @ns)
120
+ if section.section_type == 'Whole building'
121
+ @building_sections_whole_building.push(section)
122
+ elsif section.section_type == 'Space function' || section.section_type.nil?
123
+ @building_sections.push(section)
124
+ else
125
+ puts "Unknown section type found:#{section.section_type}:"
126
+ end
127
+ end
128
+
129
+ # generate building name
130
+ read_other_building_details
131
+ end
132
+
133
+ # set all function to set all parameters for this building
134
+ def set_all
135
+ if !@all_set
136
+ @all_set = true
137
+ set_bldg_and_system_type_for_building_and_section
138
+ set_building_form_defaults
139
+ set_width_and_length
140
+ end
141
+ end
142
+
143
+ # set width and length of the building footprint
144
+ def set_width_and_length
145
+ footprint = @total_floor_area / num_stories.to_f
146
+ @width = Math.sqrt(footprint / @ns_to_ew_ratio)
147
+ @length = footprint / @width
148
+ end
149
+
150
+ def check_occupancy_classification(site_occupancy_classification)
151
+ # Set the OccupancyClassification text as that defined by the Site
152
+ # ONLY if it is not already defined
153
+ if !site_occupancy_classification.nil?
154
+ xset_or_create('OccupancyClassification', site_occupancy_classification, false)
155
+ end
156
+ if xget_text('OccupancyClassification').nil?
157
+ raise StandardError, "Building ID: #{xget_id}. OccupancyClassification must be defined at either the Site or Building level."
158
+ end
159
+ end
160
+
161
+ # Set the @built_year based on YearOfConstruction / YearOfLastMajorRemodel
162
+ def set_built_year
163
+ if !@base_xml.elements["#{@ns}:YearOfConstruction"]
164
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.read_standard_template_based_on_year', 'Year of Construction is blank in your BuildingSync file.')
165
+ raise StandardError, "Building ID: #{xget_id}. Year of Construction is blank in your BuildingSync file, but is required."
166
+ end
167
+
168
+ @built_year = xget_text_as_integer('YearOfConstruction')
169
+ remodel_year = xget_text_as_integer('YearOfLastMajorRemodel')
170
+ if !remodel_year.nil? && remodel_year > @built_year
171
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_built_year', "built_year for Standards reset from #{@built_year} (YearOfConstruction) to #{remodel_year} (YearOfLastMajorRemodel).")
172
+ @built_year = remodel_year
173
+ else
174
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_built_year', "built_year for Standards set to #{@built_year} (YearOfConstruction).")
175
+ end
176
+ end
177
+
178
+ # read stories above and below grade
179
+ def read_stories_above_and_below_grade
180
+ if @base_xml.elements["#{@ns}:FloorsAboveGrade"]
181
+ @num_stories_above_grade = @base_xml.elements["#{@ns}:FloorsAboveGrade"].text.to_f
182
+ elsif @base_xml.elements["#{@ns}:ConditionedFloorsAboveGrade"]
183
+ @num_stories_above_grade = @base_xml.elements["#{@ns}:ConditionedFloorsAboveGrade"].text.to_f
184
+ else
185
+ @num_stories_above_grade = 1.0 # setDefaultValue
186
+ end
187
+
188
+ if @base_xml.elements["#{@ns}:FloorsBelowGrade"]
189
+ @num_stories_below_grade = @base_xml.elements["#{@ns}:FloorsBelowGrade"].text.to_f
190
+ elsif @base_xml.elements["#{@ns}:ConditionedFloorsBelowGrade"]
191
+ @num_stories_below_grade = @base_xml.elements["#{@ns}:ConditionedFloorsBelowGrade"].text.to_f
192
+ else
193
+ @num_stories_below_grade = 0.0 # setDefaultValue
194
+ end
195
+
196
+ if @num_stories_below_grade > 1.0
197
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.read_stories_above_and_below_grade', "Number of stories below grade is larger than 1: #{@num_stories_below_grade}, currently only one basement story is supported.")
198
+ raise StandardError, "Building ID: #{xget_id}. Number of stories below grade is > 1 (#{num_stories_below_grade}). Currently, only one story below grade is supported."
199
+ end
200
+ end
201
+
202
+ # Set the @ns_to_ew_ratio parameter using the AspectRatio element if present
203
+ def set_ns_to_ew_ratio
204
+ if @base_xml.elements["#{@ns}:AspectRatio"]
205
+ @ns_to_ew_ratio = xget_text_as_float('AspectRatio')
206
+ else
207
+ @ns_to_ew_ratio = 0.0 # setDefaultValue
208
+ end
209
+ end
210
+
211
+ # get building type
212
+ # @return [String]
213
+ def get_building_type
214
+ set_all
215
+ # try to get the bldg type at the building level, if it is nil then look at the first section
216
+ if !@standards_building_type.nil?
217
+ return @standards_building_type
218
+ else
219
+ if @building_sections.count == 0
220
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.get_building_type', 'There is no occupancy type attached to this building in your BuildingSync file.')
221
+ raise 'Error: There is no occupancy type attached to this building in your BuildingSync file.'
222
+ else
223
+ return @building_sections[0].standards_building_type
224
+ end
225
+ end
226
+ end
227
+
228
+ # get full path to epw file
229
+ # return [String]
230
+ def get_epw_file_path
231
+ return @epw_file_path
232
+ end
233
+
234
+ # set aspect ratio, floor height, and WWR
235
+ def set_building_form_defaults
236
+ # if aspect ratio, story height or wwr have argument value of 0 then use smart building type defaults
237
+ building_form_defaults = building_form_defaults(get_building_type)
238
+ if @ns_to_ew_ratio == 0.0 && !building_form_defaults.nil?
239
+ @ns_to_ew_ratio = building_form_defaults[:aspect_ratio]
240
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.set_building_form_defaults', "0.0 value for aspect ratio will be replaced with smart default for #{get_building_type} of #{building_form_defaults[:aspect_ratio]}.")
241
+ end
242
+ if @floor_height == 0.0 && !building_form_defaults.nil?
243
+ @floor_height = OpenStudio.convert(building_form_defaults[:typical_story], 'ft', 'm').get
244
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.set_building_form_defaults', "0.0 value for floor height will be replaced with smart default for #{get_building_type} of #{building_form_defaults[:typical_story]}.")
245
+ end
246
+ # because of this can't set wwr to 0.0. If that is desired then we can change this to check for 1.0 instead of 0.0
247
+ if @wwr == 0.0 && !building_form_defaults.nil?
248
+ @wwr = building_form_defaults[:wwr]
249
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.set_building_form_defaults', "0.0 value for window to wall ratio will be replaced with smart default for #{get_building_type} of #{building_form_defaults[:wwr]}.")
250
+ end
251
+ end
252
+
253
+ # check building fraction
254
+ def check_building_fraction
255
+ # check that sum of fractions for b,c, and d is less than 1.0 (so something is left for primary building type)
256
+ building_fraction = 1.0
257
+ if @building_sections.count > 0
258
+ # first we check if the building sections do have a fraction
259
+ if @building_sections.count > 1
260
+ areas = []
261
+ floor_area = 0
262
+ @building_sections.each do |section|
263
+ if section.fraction_area.nil?
264
+ areas.push(section.total_floor_area)
265
+ floor_area += section.total_floor_area
266
+ end
267
+ end
268
+ i = 0
269
+ @building_sections.each do |section|
270
+ section.fraction_area = areas[i] / @total_floor_area
271
+ i += 1
272
+ end
273
+ elsif @building_sections.count == 1
274
+ # only if we have just one section the section fraction is set to the building fraction (1)
275
+ @building_sections[0].fraction_area = building_fraction
276
+ end
277
+ @building_sections.each do |section|
278
+ puts "section with ID: #{section.xget_id} and type: '#{section.xget_text('SectionType')}' has fraction: #{section.fraction_area}"
279
+ next if section.fraction_area.nil?
280
+ building_fraction -= section.fraction_area
281
+ end
282
+ if building_fraction.round(3) < 0.0
283
+ puts "building fraction is #{building_fraction}"
284
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.check_building_fraction', 'Primary Building Type fraction of floor area must be greater than 0. Please lower one or more of the fractions for Building Type B-D.')
285
+ raise 'ERROR: Primary Building Type fraction of floor area must be greater than 0. Please lower one or more of the fractions for Building Type B-D.'
286
+ end
287
+ end
288
+ end
289
+
290
+ # read other building details
291
+ def read_other_building_details
292
+ if @base_xml.elements["#{@ns}:OccupancyLevels/#{@ns}:OccupancyLevel/#{@ns}:OccupantQuantity"]
293
+ @occupant_quantity = @base_xml.elements["#{@ns}:OccupancyLevels/#{@ns}:OccupancyLevel/#{@ns}:OccupantQuantity"].text
294
+ else
295
+ @occupant_quantity = nil
296
+ end
297
+
298
+ if @base_xml.elements["#{@ns}:SpatialUnits/#{@ns}:SpatialUnit/#{@ns}:NumberOfUnits"]
299
+ @number_of_units = @base_xml.elements["#{@ns}:SpatialUnits/#{@ns}:SpatialUnit/#{@ns}:NumberOfUnits"].text
300
+ else
301
+ @number_of_units = nil
302
+ end
303
+ end
304
+
305
+ # create building space types
306
+ # @param model [OpenStudio::Model]
307
+ def create_bldg_space_types(model)
308
+ @building_sections.each do |bldg_subsec|
309
+ bldg_subsec.create_space_types(model, @total_floor_area, num_stories, @standard_template, @open_studio_standard)
310
+ end
311
+ end
312
+
313
+ # build zone hash that stores zone lists for buildings and building sections
314
+ # @return [[hash<string, array<Zone>>]]
315
+ def build_zone_hash
316
+ zone_hash = {}
317
+ if @space_types
318
+ zone_list = []
319
+ @space_types.each do |space_name, space_type|
320
+ zone_list.concat(get_zones_per_space_type(space_type[:space_type]))
321
+ end
322
+ zone_hash[xget_id] = zone_list
323
+ end
324
+ @building_sections.each do |bldg_subsec|
325
+ zone_list = []
326
+ bldg_subsec.space_types_floor_area.each do |space_type, hash|
327
+ zone_list.concat(get_zones_per_space_type(space_type))
328
+ end
329
+ zone_hash[bldg_subsec.xget_id] = zone_list
330
+ end
331
+ return zone_hash
332
+ end
333
+
334
+ # build space types hash
335
+ # @return [hash<string, array<hash<string, string>>]
336
+ def build_space_type_hash
337
+ space_type_hash = {}
338
+ if @space_types
339
+ space_type_list = []
340
+ @space_types.each do |space_name, space_type|
341
+ space_type_list << space_type[:space_type]
342
+ end
343
+ space_type_hash[xget_id] = space_type_list
344
+ end
345
+ @building_sections.each do |bldg_subsec|
346
+ space_type_list = []
347
+ bldg_subsec.space_types_floor_area.each do |space_type, hash|
348
+ space_type_list << space_type
349
+ end
350
+ space_type_hash[bldg_subsec.xget_id] = space_type_list
351
+ end
352
+ return space_type_hash
353
+ end
354
+
355
+ # generate building space types floor area hash
356
+ # @return [Hash]
357
+ def bldg_space_types_floor_area_hash
358
+ new_hash = {}
359
+ if @building_sections.count > 0
360
+ @building_sections.each do |bldg_subsec|
361
+ bldg_subsec.space_types_floor_area.each do |space_type, hash|
362
+ new_hash[space_type] = hash
363
+ end
364
+ end
365
+ # if we have no sections we need to do the same just for the building
366
+ elsif @building_sections.count == 0
367
+ @space_types = get_space_types_from_building_type(@standards_building_type, @standard_template, true)
368
+ puts " Space types: #{@space_types} selected for building type: #{@standards_building_type} and standard template: #{@standard_template}"
369
+ space_types_floor_area = create_space_types(@model, @total_floor_area, num_stories, @standard_template, @open_studio_standard)
370
+ space_types_floor_area.each do |space_type, hash|
371
+ new_hash[space_type] = hash
372
+ end
373
+ end
374
+ return new_hash
375
+ end
376
+
377
+ # in initialize an empty model
378
+ def initialize_model
379
+ # let's create our new empty model
380
+ @model = OpenStudio::Model::Model.new if @model.nil?
381
+ end
382
+
383
+ # set building and system type for building and sections
384
+ def set_bldg_and_system_type_for_building_and_section
385
+ @building_sections.each(&:set_bldg_and_system_type)
386
+
387
+ set_bldg_and_system_type(xget_text('OccupancyClassification'), @total_floor_area, num_stories, true)
388
+ end
389
+
390
+ # determine the open studio standard and call the set_all function
391
+ # @param standard_to_be_used [String]
392
+ # @return [Standard]
393
+ def determine_open_studio_standard(standard_to_be_used)
394
+ set_all
395
+ begin
396
+ set_standard_template(standard_to_be_used, get_built_year)
397
+ building_type = get_building_type
398
+ @open_studio_standard = Standard.build("#{@standard_template}_#{building_type}")
399
+ update_name
400
+ rescue StandardError => e
401
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.determine_open_studio_standard', e.message[0..20])
402
+ raise StandardError, "BuildingSync.Building.determine_open_studio_standard: #{e.message[0..20]}"
403
+ end
404
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.determine_open_studio_standard', "Building Standard with template: #{@standard_template}_#{building_type}") if !@open_studio_standard.nil?
405
+ return @open_studio_standard
406
+ end
407
+
408
+ # update the name of the building
409
+ def update_name
410
+ # update the name so it includes the standard_template string
411
+ name_array = [@standard_template]
412
+ name_array << get_building_type
413
+ @building_sections.each do |bld_tp|
414
+ name_array << bld_tp.standards_building_type
415
+ end
416
+ name_array << @name if !@name.nil? && !@name == ''
417
+ @name = name_array.join('|').to_s
418
+ end
419
+
420
+ # set standard template
421
+ # @param standard_to_be_used [String]
422
+ # @param built_year [Integer]
423
+ def set_standard_template(standard_to_be_used, built_year)
424
+ if standard_to_be_used == CA_TITLE24
425
+ if built_year < 1978
426
+ @standard_template = 'CBES Pre-1978'
427
+ elsif built_year >= 1978 && built_year < 1992
428
+ @standard_template = 'CBES T24 1978'
429
+ elsif built_year >= 1992 && built_year < 2001
430
+ @standard_template = 'CBES T24 1992'
431
+ elsif built_year >= 2001 && built_year < 2005
432
+ @standard_template = 'CBES T24 2001'
433
+ elsif built_year >= 2005 && built_year < 2008
434
+ @standard_template = 'CBES T24 2005'
435
+ else
436
+ @standard_template = 'CBES T24 2008'
437
+ end
438
+ elsif standard_to_be_used == ASHRAE90_1
439
+ if built_year < 1980
440
+ @standard_template = 'DOE Ref Pre-1980'
441
+ elsif built_year >= 1980 && built_year < 2004
442
+ @standard_template = 'DOE Ref 1980-2004'
443
+ elsif built_year >= 2004 && built_year < 2007
444
+ @standard_template = '90.1-2004'
445
+ elsif built_year >= 2007 && built_year < 2010
446
+ @standard_template = '90.1-2007'
447
+ elsif built_year >= 2010 && built_year < 2013
448
+ @standard_template = '90.1-2010'
449
+ elsif built_year >= 2013
450
+ @standard_template = '90.1-2013'
451
+ end
452
+ # TODO: add ASHRAE 2016 once it is available
453
+ else
454
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.get_standard_template', "Unknown standard_to_be_used #{standard_to_be_used}.")
455
+ raise StandardError, "BuildingSync.Building.get_standard_template: Unknown standard_to_be_used #{standard_to_be_used}."
456
+ end
457
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.get_standard_template', "Using the following standard for default values #{@standard_template}.")
458
+ end
459
+
460
+ # get zones per space type
461
+ # @param space_type [OpenStudio::Model::SpaceType]
462
+ # @return [array<OpenStudio::Model::ThermalZone>]
463
+ def get_zones_per_space_type(space_type)
464
+ list_of_zones = []
465
+ model_space_type = @model.getSpaceTypeByName(space_type.name.get).get
466
+ model_space_type.spaces.each do |space|
467
+ list_of_zones << space.thermalZone.get
468
+ end
469
+ return list_of_zones
470
+ end
471
+
472
+ # get model
473
+ # @return [OpenStudio::Model]
474
+ def get_model
475
+ # in case the model was not initialized before we create a new model if it is nil
476
+ initialize_model
477
+ return @model
478
+ end
479
+
480
+ # get year building was built
481
+ # @return [Integer]
482
+ def get_built_year
483
+ return @built_year
484
+ end
485
+
486
+ # get @standard_template
487
+ # @return [String]
488
+ def get_standard_template
489
+ return @standard_template
490
+ end
491
+
492
+ # get system type
493
+ # @return [String]
494
+ def get_system_type
495
+ set_all
496
+ if !@system_type.nil?
497
+ return @system_type
498
+ else
499
+ return @building_sections[0].system_type
500
+ end
501
+ end
502
+
503
+ # get stat file path
504
+ # @param epw_file [String]
505
+ # @return [String]
506
+ def get_stat_file(epw_file)
507
+ # Add SiteWaterMainsTemperature -- via parsing of STAT file.
508
+ stat_file = "#{File.join(File.dirname(epw_file.path.to_s), File.basename(epw_file.path.to_s, '.*'))}.stat"
509
+ unless File.exist? stat_file
510
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.get_stat_file', 'Could not find STAT file by filename, looking in the directory')
511
+ stat_files = Dir["#{File.dirname(epw_file.path.to_s)}/*.stat"]
512
+ if stat_files.size > 1
513
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.get_stat_file', 'More than one stat file in the EPW directory')
514
+ return nil
515
+ end
516
+ if stat_files.empty?
517
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.get_stat_file', 'Cound not find the stat file in the EPW directory')
518
+ return nil
519
+ end
520
+
521
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.get_stat_file', "Using STAT file: #{stat_files.first}")
522
+ stat_file = stat_files.first
523
+ end
524
+ unless stat_file
525
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.get_stat_file', 'Could not find stat file')
526
+ return nil
527
+ end
528
+ return stat_file
529
+ end
530
+
531
+ # set weather file and climate zone
532
+ # @param climate_zone [String]
533
+ # @param epw_file_path [String]
534
+ # @param standard_to_be_used [String]
535
+ # @param latitude [String]
536
+ # @param longitude [String]
537
+ # @param ddy_file [String]
538
+ # @param weather_argb [array]
539
+ def set_weather_and_climate_zone(climate_zone, epw_file_path, standard_to_be_used, latitude, longitude, ddy_file, *weather_argb)
540
+ initialize_model
541
+
542
+ determine_climate_zone(standard_to_be_used) if climate_zone.nil?
543
+
544
+ # here we check if there is an valid EPW file, if there is we use that file otherwise everything will be generated from climate zone
545
+ if !epw_file_path.nil? && File.exist?(epw_file_path)
546
+ @epw_file_path = epw_file_path
547
+ puts "case 1: epw file exists #{epw_file_path} and climate_zone is: #{climate_zone}"
548
+ set_weather_and_climate_zone_from_epw(climate_zone, standard_to_be_used, latitude, longitude, ddy_file)
549
+ elsif climate_zone.nil? && @climate_zone.nil?
550
+ weather_station_id = weather_argb[1]
551
+ state_name = weather_argb[2]
552
+ city_name = weather_argb[3]
553
+ puts 'case 2: climate_zone is nil at the Site and Building level'
554
+ if !weather_station_id.nil?
555
+ puts "case 2.1: weather_station_id is not nil #{weather_station_id}"
556
+ @epw_file_path = BuildingSync::GetBCLWeatherFile.new.download_weather_file_from_weather_id(weather_station_id)
557
+ elsif !city_name.nil? && !state_name.nil?
558
+ puts "case 2.2: SITE LEVEL city_name and state_name is not nil #{city_name} #{state_name}"
559
+ @epw_file_path = BuildingSync::GetBCLWeatherFile.new.download_weather_file_from_city_name(state_name, city_name)
560
+ elsif !@city_name.nil? && !@state_name.nil?
561
+ puts "case 2.3: BUILDING LEVEL city_name and state_name is not nil #{@city_name} #{@state_name}"
562
+ @epw_file_path = BuildingSync::GetBCLWeatherFile.new.download_weather_file_from_city_name(@state_name, @city_name)
563
+ end
564
+
565
+ else
566
+ puts "case 3: SITE LEVEL climate zone #{climate_zone} BUILDING LEVEL climate zone #{@climate_zone}."
567
+ puts "lat #{latitude} long #{longitude}"
568
+ if climate_zone.nil?
569
+ climate_zone = @climate_zone
570
+ puts "Climate Zone set at the Building level: #{climate_zone}"
571
+ else
572
+ puts "Climate Zone set at the Site level: #{climate_zone}"
573
+ end
574
+ @epw_file_path = set_weather_and_climate_zone_from_climate_zone(climate_zone, standard_to_be_used, latitude, longitude)
575
+ @epw_file_path = @epw_file_path.to_s
576
+ end
577
+
578
+ # Ensure a file path gets set, else raise error
579
+ if @epw_file_path.nil?
580
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.set_weather_and_climate_zone', 'epw_file_path is nil and no way to set from Site or Building parameters.')
581
+ raise StandardError, 'BuildingSync.Building.set_weather_and_climate_zone: epw_file_path is nil and no way to set from Site or Building parameters.'
582
+ elsif !@epw_file_path
583
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.set_weather_and_climate_zone', "epw_file_path is false: #{@epw_file_path}")
584
+ raise StandardError, "BuildingSync.Building.set_weather_and_climate_zone: epw_file_path is false: #{@epw_file_path}"
585
+ elsif !File.exist?(@epw_file_path)
586
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.set_weather_and_climate_zone', "epw_file_path does not exist: #{@epw_file_path}")
587
+ raise StandardError, "BuildingSync.Building.set_weather_and_climate_zone: epw_file_path does not exist: #{@epw_file_path}"
588
+ end
589
+
590
+ # setting the current year, so we do not get these annoying log messages:
591
+ # [openstudio.model.YearDescription] <1> 'UseWeatherFile' is not yet a supported option for YearDescription
592
+ year_description = @model.getYearDescription
593
+ year_description.setCalendarYear(::Date.today.year)
594
+
595
+ # add final condition
596
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_weather_and_climate_zone', "The final weather file is #{@model.getWeatherFile.city} and the model has #{@model.getDesignDays.size} design day objects.")
597
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_weather_and_climate_zone', "The path to the epw file is: #{@epw_file_path}")
598
+ end
599
+
600
+ # set weather file and climate zone from climate zone
601
+ # @param climate_zone [String]
602
+ # @param standard_to_be_used [String]
603
+ # @param latitude [String]
604
+ # @param longitude [String]
605
+ def set_weather_and_climate_zone_from_climate_zone(climate_zone, standard_to_be_used, latitude, longitude)
606
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.set_weather_and_climate_zone_from_climate_zone', "Cannot add design days and weather file for climate zone: #{climate_zone}, no epw file provided")
607
+ climate_zone_standard_string = climate_zone
608
+ puts climate_zone
609
+ puts standard_to_be_used
610
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_weather_and_climate_zone_from_climate_zone', "climate zone: #{climate_zone}")
611
+ if standard_to_be_used == CA_TITLE24 && !climate_zone.nil?
612
+ climate_zone_standard_string = "CEC T24-CEC#{climate_zone.gsub('Climate Zone', '').strip}"
613
+ elsif standard_to_be_used == ASHRAE90_1 && !climate_zone.nil?
614
+ climate_zone_standard_string = "ASHRAE 169-2006-#{climate_zone.gsub('Climate Zone', '').strip}"
615
+ elsif climate_zone.nil?
616
+ climate_zone_standard_string = ''
617
+ end
618
+
619
+ puts @open_studio_standard
620
+ puts @open_studio_standard.class
621
+ puts climate_zone_standard_string
622
+ if !@open_studio_standard.nil? && !@open_studio_standard.model_add_design_days_and_weather_file(@model, climate_zone_standard_string, nil)
623
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.set_weather_and_climate_zone_from_climate_zone', "Cannot add design days and weather file for climate zone: #{climate_zone}, no epw file provided")
624
+ end
625
+
626
+ # overwrite latitude and longitude if available
627
+ if !latitude.nil? || !longitude.nil?
628
+ site = @model.getSite
629
+ if !latitude.nil?
630
+ site.setLatitude(latitude.to_f)
631
+ end
632
+ if !longitude.nil?
633
+ site.setLongitude(longitude.to_f)
634
+ end
635
+ end
636
+
637
+ weather_file = @model.getWeatherFile
638
+
639
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_weather_and_climate_zone_from_climate_zone', "city is #{weather_file.city}. State is #{weather_file.stateProvinceRegion}")
640
+
641
+ set_climate_zone(climate_zone, standard_to_be_used)
642
+ return weather_file.path.get
643
+ end
644
+
645
+ # set climate zone
646
+ # @param climate_zone [String]
647
+ # @param standard_to_be_used [String]
648
+ # @param stat_file [String]
649
+ # @return [Boolean]
650
+ def set_climate_zone(climate_zone, standard_to_be_used, stat_file = nil)
651
+ # Set climate zone
652
+ if climate_zone.nil?
653
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.set_climate_zone', 'Climate Zone is nil, trying to get it from stat file')
654
+ # get climate zone from stat file
655
+ text = nil
656
+ File.open(stat_file) do |f|
657
+ text = f.read.force_encoding('iso-8859-1')
658
+ end
659
+
660
+ # Get Climate zone.
661
+ # - Climate type "3B" (ASHRAE Standard 196-2006 Climate Zone)**
662
+ # - Climate type "6A" (ASHRAE Standards 90.1-2004 and 90.2-2004 Climate Zone)**
663
+ regex = /Climate type \"(.*?)\" \(ASHRAE Standards?(.*)\)\*\*/
664
+ match_data = text.match(regex)
665
+ if match_data.nil?
666
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.set_climate_zone', "Can't find ASHRAE climate zone in stat file.")
667
+ else
668
+ climate_zone = match_data[1].to_s.strip
669
+ end
670
+ end
671
+
672
+ climate_zones = @model.getClimateZones
673
+ # set climate zone
674
+ climate_zones.clear
675
+ if standard_to_be_used == ASHRAE90_1 && !climate_zone.nil?
676
+ climate_zones.setClimateZone('ASHRAE', climate_zone)
677
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_climate_zone', "Setting Climate Zone to #{climate_zones.getClimateZones('ASHRAE').first.value}")
678
+ puts "setting ASHRAE climate zone to: #{climate_zone}"
679
+ return true
680
+ elsif standard_to_be_used == CA_TITLE24 && !climate_zone.nil?
681
+ climate_zone = climate_zone.gsub('CEC', '').strip
682
+ climate_zone = climate_zone.gsub('Climate Zone', '').strip
683
+ climate_zone = climate_zone.delete('A').strip
684
+ climate_zone = climate_zone.delete('B').strip
685
+ climate_zone = climate_zone.delete('C').strip
686
+ climate_zones.setClimateZone('CEC', climate_zone)
687
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_climate_zone', "Setting Climate Zone to #{climate_zone}")
688
+ puts "setting CA_TITLE24 climate zone to: #{climate_zone}"
689
+ return true
690
+ end
691
+ puts "could not set climate_zone #{climate_zone}"
692
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.set_climate_zone', "Cannot set the #{climate_zone} in context of this standard #{standard_to_be_used}")
693
+ return false
694
+ end
695
+
696
+ # set weather file and climate zone from EPW file
697
+ # @param climate_zone [String]
698
+ # @param standard_to_be_used [String]
699
+ # @param latitude [String]
700
+ # @param longitude [String]
701
+ # @param ddy_file [String]
702
+ def set_weather_and_climate_zone_from_epw(climate_zone, standard_to_be_used, latitude, longitude, ddy_file = nil)
703
+ epw_file = OpenStudio::EpwFile.new(@epw_file_path)
704
+
705
+ weather_lat = epw_file.latitude
706
+ if !latitude.nil?
707
+ weather_lat = latitude.to_f
708
+ end
709
+ weather_lon = epw_file.longitude
710
+ if !longitude.nil?
711
+ weather_lon = longitude.to_f
712
+ end
713
+
714
+ weather_file = @model.getWeatherFile
715
+ weather_file.setCity(epw_file.city)
716
+ weather_file.setStateProvinceRegion(epw_file.stateProvinceRegion)
717
+ weather_file.setCountry(epw_file.country)
718
+ weather_file.setDataSource(epw_file.dataSource)
719
+ weather_file.setWMONumber(epw_file.wmoNumber.to_s)
720
+ weather_file.setLatitude(weather_lat)
721
+ weather_file.setLongitude(weather_lon)
722
+ weather_file.setTimeZone(epw_file.timeZone)
723
+ weather_file.setElevation(epw_file.elevation)
724
+ weather_file.setString(10, epw_file.path.to_s)
725
+
726
+ weather_name = "#{epw_file.city}_#{epw_file.stateProvinceRegion}_#{epw_file.country}"
727
+ weather_time = epw_file.timeZone
728
+ weather_elev = epw_file.elevation
729
+
730
+ # Add or update site data
731
+ site = @model.getSite
732
+ site.setName(weather_name)
733
+ site.setLatitude(weather_lat)
734
+ site.setLongitude(weather_lon)
735
+ site.setTimeZone(weather_time)
736
+ site.setElevation(weather_elev)
737
+
738
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_weather_and_climate_zone_from_epw', "city is #{epw_file.city}. State is #{epw_file.stateProvinceRegion}")
739
+
740
+ stat_file = get_stat_file(epw_file)
741
+ add_site_water_mains_temperature(stat_file) if !stat_file.nil?
742
+
743
+ set_climate_zone(climate_zone, standard_to_be_used, stat_file)
744
+
745
+ # Remove all the Design Day objects that are in the file
746
+ @model.getObjectsByType('OS:SizingPeriod:DesignDay'.to_IddObjectType).each(&:remove)
747
+
748
+ # find the ddy files
749
+ ddy_file = "#{File.join(File.dirname(epw_file.path.to_s), File.basename(epw_file.path.to_s, '.*'))}.ddy" if ddy_file.nil?
750
+ unless File.exist? ddy_file
751
+ ddy_files = Dir["#{File.dirname(epw_file.path.to_s)}/*.ddy"]
752
+ if ddy_files.size > 1
753
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.set_weather_and_climate_zone_from_epw', 'More than one ddy file in the EPW directory')
754
+ return false
755
+ end
756
+ if ddy_files.empty?
757
+ OpenStudio.logFree(OpenStudio::Error, 'BuildingSync.Building.set_weather_and_climate_zone_from_epw', 'could not find the ddy file in the EPW directory')
758
+ return false
759
+ end
760
+
761
+ ddy_file = ddy_files.first
762
+ end
763
+
764
+ unless ddy_file
765
+ runner.registerError "Could not find DDY file for #{ddy_file}"
766
+ return error
767
+ end
768
+
769
+ ddy_model = OpenStudio::EnergyPlus.loadAndTranslateIdf(ddy_file).get
770
+ ddy_model.getObjectsByType('OS:SizingPeriod:DesignDay'.to_IddObjectType).each do |d|
771
+ # grab only the ones that matter
772
+ ddy_list = /(Htg 99.6. Condns DB)|(Clg .4. Condns WB=>MDB)|(Clg .4% Condns DB=>MWB)/
773
+ if d.name.get.match?(ddy_list)
774
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.set_weather_and_climate_zone_from_epw', "Adding object #{d.name}")
775
+
776
+ # add the object to the existing model
777
+ @model.addObject(d.clone)
778
+ end
779
+ end
780
+ end
781
+
782
+ # add site water mains temperature -- via parsing of STAT file.
783
+ # @param stat_file [String]
784
+ # @return [Boolean]
785
+ def add_site_water_mains_temperature(stat_file)
786
+ stat_model = ::EnergyPlus::StatFile.new(stat_file)
787
+ water_temp = @model.getSiteWaterMainsTemperature
788
+ water_temp.setAnnualAverageOutdoorAirTemperature(stat_model.mean_dry_bulb)
789
+ water_temp.setMaximumDifferenceInMonthlyAverageOutdoorAirTemperatures(stat_model.delta_dry_bulb)
790
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.add_site_water_mains_temperature', "mean dry bulb is #{stat_model.mean_dry_bulb}")
791
+ return true
792
+ end
793
+
794
+ # generate baseline model in osm file format
795
+ def generate_baseline_osm
796
+ # checking that the fractions add up
797
+ check_building_fraction
798
+
799
+ # set building rotation
800
+ initial_rotation = @model.getBuilding.northAxis
801
+ if @building_rotation != initial_rotation
802
+ @model.getBuilding.setNorthAxis(building_rotation)
803
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.generate_baseline_osm', "Set Building Rotation to #{@model.getBuilding.northAxis}")
804
+ end
805
+ if !@name.nil?
806
+ @model.getBuilding.setName(@name)
807
+ end
808
+
809
+ create_bldg_space_types(@model)
810
+
811
+ # create envelope
812
+ # populate bar_hash and create envelope with data from envelope_data_hash and user arguments
813
+ bar_hash = {}
814
+ bar_hash[:length] = @length
815
+ bar_hash[:width] = @width
816
+ bar_hash[:num_stories_below_grade] = num_stories_below_grade.to_i
817
+ bar_hash[:num_stories_above_grade] = num_stories_above_grade.to_i
818
+ bar_hash[:floor_height] = floor_height
819
+ bar_hash[:center_of_footprint] = OpenStudio::Point3d.new(0, 0, 0)
820
+ bar_hash[:bar_division_method] = 'Multiple Space Types - Individual Stories Sliced'
821
+ # default for now 'Multiple Space Types - Individual Stories Sliced', 'Multiple Space Types - Simple Sliced', 'Single Space Type - Core and Perimeter'
822
+ bar_hash[:make_mid_story_surfaces_adiabatic] = false
823
+ bar_hash[:space_types] = bldg_space_types_floor_area_hash
824
+ bar_hash[:building_wwr_n] = wwr
825
+ bar_hash[:building_wwr_s] = wwr
826
+ bar_hash[:building_wwr_e] = wwr
827
+ bar_hash[:building_wwr_w] = wwr
828
+
829
+ runner = OpenStudio::Measure::OSRunner.new(OpenStudio::WorkflowJSON.new)
830
+ # remove non-resource objects not removed by removing the building
831
+ remove_non_resource_objects(runner, @model)
832
+
833
+ # party_walls_array to be used by orientation specific or fractional party wall values
834
+ party_walls_array = generate_party_walls # this is an array of arrays, where each entry is effective building story with array of directions
835
+
836
+ # populate bar hash with story information
837
+ bar_hash[:stories] = {}
838
+ num_stories.ceil.times do |i|
839
+ if party_walls_array.empty?
840
+ party_walls = []
841
+ else
842
+ party_walls = party_walls_array[i]
843
+ end
844
+
845
+ # add below_partial_story
846
+ if num_stories.ceil > num_stories && i == num_stories_round_up - 2
847
+ below_partial_story = true
848
+ else
849
+ below_partial_story = false
850
+ end
851
+
852
+ # bottom_story_ground_exposed_floor and top_story_exterior_exposed_roof already setup as bool
853
+ bar_hash[:stories]["key #{i}"] = { story_party_walls: party_walls, story_min_multiplier: 1, story_included_in_building_area: true, below_partial_story: below_partial_story, bottom_story_ground_exposed_floor: true, top_story_exterior_exposed_roof: true }
854
+ end
855
+
856
+ # store expected floor areas to check after bar made
857
+ target_areas = {}
858
+ bar_hash[:space_types].each do |k, v|
859
+ target_areas[k] = v[:floor_area]
860
+ end
861
+
862
+ # create bar
863
+ create_bar(runner, @model, bar_hash, 'Basements Ground Mid Top')
864
+ # using the default value for story multiplier for now 'Basements Ground Mid Top'
865
+
866
+ # check expected floor areas against actual
867
+ @model.getSpaceTypes.sort.each do |space_type|
868
+ next if !target_areas.key? space_type
869
+
870
+ # convert to IP
871
+ actual_ip = OpenStudio.convert(space_type.floorArea, 'm^2', 'ft^2').get
872
+ target_ip = OpenStudio.convert(target_areas[space_type], 'm^2', 'ft^2').get
873
+
874
+ if (space_type.floorArea - target_areas[space_type]).abs >= 1.0
875
+ if !bar_hash[:bar_division_method].include? 'Single Space Type'
876
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.generate_baseline_osm', "#{space_type.name} doesn't have the expected floor area (actual #{OpenStudio.toNeatString(actual_ip, 0, true)} ft^2, target #{OpenStudio.toNeatString(target_ip, 0, true)} ft^2)")
877
+ return false
878
+ else
879
+ # will see this if use Single Space type division method on multi-use building or single building type without whole building space type
880
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.generate_baseline_osm', "WARNING: #{space_type.name} doesn't have the expected floor area (actual #{OpenStudio.toNeatString(actual_ip, 0, true)} ft^2, target #{OpenStudio.toNeatString(target_ip, 0, true)} ft^2)")
881
+ end
882
+ end
883
+ end
884
+
885
+ # test for excessive exterior roof area (indication of problem with intersection and or surface matching)
886
+ ext_roof_area = @model.getBuilding.exteriorSurfaceArea - @model.getBuilding.exteriorWallArea
887
+ expected_roof_area = total_floor_area / num_stories.to_f
888
+ if ext_roof_area > expected_roof_area # only test if using whole-building area input
889
+ OpenStudio.logFree(OpenStudio::Warn, 'BuildingSync.Building.generate_baseline_osm', 'Roof area larger than expected, may indicate problem with inter-floor surface intersection or matching.')
890
+ return false
891
+ end
892
+
893
+ # report final condition of model
894
+ OpenStudio.logFree(OpenStudio::Info, 'BuildingSync.Building.generate_baseline_osm', "The building finished with #{@model.getSpaces.size} spaces.")
895
+
896
+ return true
897
+ end
898
+
899
+ # generate party walls
900
+ def generate_party_walls
901
+ party_walls_array = []
902
+ if @party_wall_stories_north + @party_wall_stories_south + @party_wall_stories_east + @party_wall_stories_west > 0
903
+
904
+ # loop through effective number of stories add orientation specific party walls per user arguments
905
+ num_stories.ceil.times do |i|
906
+ test_value = i + 1 - bar_hash[:num_stories_below_grade]
907
+
908
+ array = []
909
+ if @party_wall_stories_north >= test_value
910
+ array << 'north'
911
+ end
912
+ if @party_wall_stories_south >= test_value
913
+ array << 'south'
914
+ end
915
+ if @party_wall_stories_east >= test_value
916
+ array << 'east'
917
+ end
918
+ if @party_wall_stories_west >= test_value
919
+ array << 'west'
920
+ end
921
+
922
+ # populate party_wall_array for this story
923
+ party_walls_array << array
924
+ end
925
+ end
926
+
927
+ # calculate party walls if using party_wall_fraction method
928
+ if @party_wall_fraction > 0 && !party_walls_array.empty?
929
+ runner.registerWarning('Both orientaiton and fractional party wall values arguments were populated, will ignore fractional party wall input')
930
+ elsif @party_wall_fraction > 0
931
+
932
+ # orientation of long and short side of building will vary based on building rotation
933
+
934
+ # full story ext wall area
935
+ typical_length_facade_area = @length * floor_height
936
+ typical_width_facade_area = @width * floor_height
937
+
938
+ # top story ext wall area, may be partial story
939
+ partial_story_multiplier = (1.0 - @num_stories_above_grade.ceil + @num_stories_above_grade)
940
+ area_multiplier = partial_story_multiplier
941
+ edge_multiplier = Math.sqrt(area_multiplier)
942
+ top_story_length = @length * edge_multiplier
943
+ top_story_width = @width * edge_multiplier
944
+ top_story_length_facade_area = top_story_length * floor_height
945
+ top_story_width_facade_area = top_story_width * floor_height
946
+
947
+ total_exterior_wall_area = 2 * (@length + @width) * (@num_stories_above_grade.ceil - 1.0) * floor_height + 2 * (top_story_length + top_story_width) * floor_height
948
+ target_party_wall_area = total_exterior_wall_area * @party_wall_fraction
949
+
950
+ width_counter = 0
951
+ width_area = 0.0
952
+ facade_area = typical_width_facade_area
953
+ until (width_area + facade_area >= target_party_wall_area) || (width_counter == @num_stories_above_grade.ceil * 2)
954
+ # update facade area for top story
955
+ if width_counter == @num_stories_above_grade.ceil - 1 || width_counter == @num_stories_above_grade.ceil * 2 - 1
956
+ facade_area = top_story_width_facade_area
957
+ else
958
+ facade_area = typical_width_facade_area
959
+ end
960
+
961
+ width_counter += 1
962
+ width_area += facade_area
963
+
964
+ end
965
+ width_area_remainder = target_party_wall_area - width_area
966
+
967
+ length_counter = 0
968
+ length_area = 0.0
969
+ facade_area = typical_length_facade_area
970
+ until (length_area + facade_area >= target_party_wall_area) || (length_counter == @num_stories_above_grade.ceil * 2)
971
+ # update facade area for top story
972
+ if length_counter == @num_stories_above_grade.ceil - 1 || length_counter == @num_stories_above_grade.ceil * 2 - 1
973
+ facade_area = top_story_length_facade_area
974
+ else
975
+ facade_area = typical_length_facade_area
976
+ end
977
+
978
+ length_counter += 1
979
+ length_area += facade_area
980
+ end
981
+ length_area_remainder = target_party_wall_area - length_area
982
+
983
+ # get rotation and best fit to adjust orientation for fraction party wall
984
+ rotation = @building_rotation % 360.0 # should result in value between 0 and 360
985
+ card_dir_array = [0.0, 90.0, 180.0, 270.0, 360.0]
986
+ # reverse array to properly handle 45, 135, 225, and 315
987
+ best_fit = card_dir_array.reverse.min_by { |x| (x.to_f - rotation).abs }
988
+
989
+ if ![90.0, 270.0].include? best_fit
990
+ width_card_dir = ['east', 'west']
991
+ length_card_dir = ['north', 'south']
992
+ else
993
+ # if rotation is closest to 90 or 270 then reverse which orientation is used for length and width
994
+ width_card_dir = ['north', 'south']
995
+ length_card_dir = ['east', 'west']
996
+ end
997
+
998
+ # if dont' find enough on short sides
999
+ if width_area_remainder <= typical_length_facade_area
1000
+
1001
+ num_stories.ceil.times do |i|
1002
+ if i + 1 <= @num_stories_below_grade
1003
+ party_walls_array << []
1004
+ next
1005
+ end
1006
+ if i + 1 - @num_stories_below_grade <= width_counter
1007
+ if i + 1 - @num_stories_below_grade <= width_counter - @num_stories_above_grade
1008
+ party_walls_array << width_card_dir
1009
+ else
1010
+ party_walls_array << [width_card_dir.first]
1011
+ end
1012
+ else
1013
+ party_walls_array << []
1014
+ end
1015
+ end
1016
+
1017
+ else
1018
+ # use long sides instead
1019
+ num_stories.ceil.times do |i|
1020
+ if i + 1 <= @num_stories_below_grade
1021
+ party_walls_array << []
1022
+ next
1023
+ end
1024
+ if i + 1 - @num_stories_below_grade <= length_counter
1025
+ if i + 1 - @num_stories_below_grade <= length_counter - @num_stories_above_grade
1026
+ party_walls_array << length_card_dir
1027
+ else
1028
+ party_walls_array << [length_card_dir.first]
1029
+ end
1030
+ else
1031
+ party_walls_array << []
1032
+ end
1033
+ end
1034
+ end
1035
+ # TODO: - currently won't go past making two opposing sets of walls party walls. Info and registerValue are after create_bar in measure.rb
1036
+ end
1037
+ party_walls_array
1038
+ end
1039
+
1040
+ # write baseline model to osm file
1041
+ # @param dir [String]
1042
+ def write_osm(dir)
1043
+ @model.save("#{dir}/in.osm", true)
1044
+ end
1045
+
1046
+ # write parameters to xml file
1047
+ def prepare_final_xml
1048
+ @base_xml.elements["#{@ns}:OccupancyLevels/#{@ns}:OccupancyLevel/#{@ns}:OccupantQuantity"].text = @occupant_quantity if !@occupant_quantity.nil?
1049
+ @base_xml.elements["#{@ns}:SpatialUnits/#{@ns}:SpatialUnit/#{@ns}:NumberOfUnits"].text = @number_of_units if !@number_of_units.nil?
1050
+
1051
+ # Add new element in the XML file
1052
+ add_user_defined_field_to_xml_file('OpenStudioModelName', @name)
1053
+ add_user_defined_field_to_xml_file('StandardTemplateYearOfConstruction', @built_year)
1054
+ add_user_defined_field_to_xml_file('StandardTemplate', @standard_template)
1055
+ add_user_defined_field_to_xml_file('BuildingRotation', @building_rotation)
1056
+ add_user_defined_field_to_xml_file('FloorHeight', @floor_height)
1057
+ add_user_defined_field_to_xml_file('WindowWallRatio', @wwr)
1058
+ add_user_defined_field_to_xml_file('PartyWallStoriesNorth', @party_wall_stories_north)
1059
+ add_user_defined_field_to_xml_file('PartyWallStoriesSouth', @party_wall_stories_south)
1060
+ add_user_defined_field_to_xml_file('PartyWallStoriesEast', @party_wall_stories_east)
1061
+ add_user_defined_field_to_xml_file('PartyWallStoriesWest', @party_wall_stories_west)
1062
+ add_user_defined_field_to_xml_file('Width', @width)
1063
+ add_user_defined_field_to_xml_file('Length', @length)
1064
+ add_user_defined_field_to_xml_file('PartyWallFraction', @party_wall_fraction)
1065
+ add_user_defined_field_to_xml_file('ModelNumberThermalZones', @model.getThermalZones.size)
1066
+ add_user_defined_field_to_xml_file('ModelNumberSpaces', @model.getSpaces.size)
1067
+ add_user_defined_field_to_xml_file('ModelNumberStories', @model.getBuildingStorys.size)
1068
+ add_user_defined_field_to_xml_file('ModelNumberPeople', @model.getBuilding.numberOfPeople)
1069
+ add_user_defined_field_to_xml_file('ModelFloorArea(m2)', @model.getBuilding.floorArea)
1070
+
1071
+ wf = @model.weatherFile.get
1072
+ add_user_defined_field_to_xml_file('ModelWeatherFileName', wf.nameString)
1073
+ add_user_defined_field_to_xml_file('ModelWeatherFileDataSource', wf.dataSource)
1074
+ add_user_defined_field_to_xml_file('ModelWeatherFileCity', wf.city)
1075
+ add_user_defined_field_to_xml_file('ModelWeatherFileStateProvinceRegion', wf.stateProvinceRegion)
1076
+ add_user_defined_field_to_xml_file('ModelWeatherFileLatitude', wf.latitude)
1077
+ add_user_defined_field_to_xml_file('ModelWeatherFileLongitude', wf.longitude)
1078
+ prepare_final_xml_for_spatial_element
1079
+ end
1080
+
1081
+ # get space types
1082
+ # @return [array<OpenStudio::Model::SpaceType>]
1083
+ def get_space_types
1084
+ return @model.getSpaceTypes
1085
+ end
1086
+
1087
+ # get peak occupancy
1088
+ # @return [hash<string, float>]
1089
+ def get_peak_occupancy
1090
+ peak_occupancy = {}
1091
+ if @occupant_quantity
1092
+ peak_occupancy[xget_id] = @occupant_quantity.to_f
1093
+ return peak_occupancy
1094
+ end
1095
+ @building_sections.each do |section|
1096
+ peak_occupancy[section.xget_id] = section.get_peak_occupancy.to_f if section.get_peak_occupancy
1097
+ end
1098
+ return peak_occupancy
1099
+ end
1100
+
1101
+ # get floor area
1102
+ # @return [hash<string, float>]
1103
+ def get_floor_area
1104
+ floor_area = {}
1105
+ if @total_floor_area
1106
+ floor_area[xget_id] = @total_floor_area.to_f
1107
+ end
1108
+ @building_sections.each do |section|
1109
+ if section.get_floor_area
1110
+ floor_area[section.xget_id] = section.get_floor_area
1111
+ end
1112
+ end
1113
+ return floor_area
1114
+ end
1115
+
1116
+ attr_reader :building_rotation, :name, :length, :width, :num_stories_above_grade, :num_stories_below_grade, :floor_height, :space, :wwr,
1117
+ :occupant_quantity, :number_of_units, :built_year, :year_major_remodel, :building_sections
1118
+ end
1119
+ end