buildingsync 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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