urbanopt-reporting 0.1.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +24 -0
  3. data/.rubocop.yml +10 -0
  4. data/CHANGELOG.md +7 -0
  5. data/CONTRIBUTING.md +58 -0
  6. data/Gemfile +18 -0
  7. data/Jenkinsfile +10 -0
  8. data/LICENSE.md +27 -0
  9. data/README.md +40 -0
  10. data/Rakefile +45 -0
  11. data/doc_templates/LICENSE.md +27 -0
  12. data/doc_templates/README.md.erb +42 -0
  13. data/doc_templates/copyright_erb.txt +31 -0
  14. data/doc_templates/copyright_js.txt +4 -0
  15. data/doc_templates/copyright_ruby.txt +29 -0
  16. data/lib/measures/.rubocop.yml +5 -0
  17. data/lib/measures/default_feature_reports/LICENSE.md +27 -0
  18. data/lib/measures/default_feature_reports/README.md +26 -0
  19. data/lib/measures/default_feature_reports/README.md.erb +42 -0
  20. data/lib/measures/default_feature_reports/measure.rb +1012 -0
  21. data/lib/measures/default_feature_reports/measure.xml +160 -0
  22. data/lib/urbanopt/reporting.rb +37 -0
  23. data/lib/urbanopt/reporting/default_reports.rb +44 -0
  24. data/lib/urbanopt/reporting/default_reports/construction_cost.rb +169 -0
  25. data/lib/urbanopt/reporting/default_reports/date.rb +97 -0
  26. data/lib/urbanopt/reporting/default_reports/distributed_generation.rb +379 -0
  27. data/lib/urbanopt/reporting/default_reports/end_use.rb +159 -0
  28. data/lib/urbanopt/reporting/default_reports/end_uses.rb +140 -0
  29. data/lib/urbanopt/reporting/default_reports/extension.rb +15 -0
  30. data/lib/urbanopt/reporting/default_reports/feature_report.rb +266 -0
  31. data/lib/urbanopt/reporting/default_reports/generator.rb +92 -0
  32. data/lib/urbanopt/reporting/default_reports/location.rb +99 -0
  33. data/lib/urbanopt/reporting/default_reports/logger.rb +44 -0
  34. data/lib/urbanopt/reporting/default_reports/power_distribution.rb +103 -0
  35. data/lib/urbanopt/reporting/default_reports/program.rb +265 -0
  36. data/lib/urbanopt/reporting/default_reports/reporting_period.rb +300 -0
  37. data/lib/urbanopt/reporting/default_reports/scenario_report.rb +317 -0
  38. data/lib/urbanopt/reporting/default_reports/schema/README.md +33 -0
  39. data/lib/urbanopt/reporting/default_reports/schema/scenario_csv_columns.txt +34 -0
  40. data/lib/urbanopt/reporting/default_reports/schema/scenario_schema.json +857 -0
  41. data/lib/urbanopt/reporting/default_reports/solar_pv.rb +93 -0
  42. data/lib/urbanopt/reporting/default_reports/storage.rb +105 -0
  43. data/lib/urbanopt/reporting/default_reports/timeseries_csv.rb +300 -0
  44. data/lib/urbanopt/reporting/default_reports/validator.rb +112 -0
  45. data/lib/urbanopt/reporting/default_reports/wind.rb +92 -0
  46. data/lib/urbanopt/reporting/derived_extension.rb +63 -0
  47. data/lib/urbanopt/reporting/version.rb +35 -0
  48. data/urbanopt-reporting-gem.gemspec +33 -0
  49. metadata +176 -0
@@ -0,0 +1,93 @@
1
+ # *********************************************************************************
2
+ # URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
3
+ # contributors. All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice, this list
9
+ # of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright notice, this
12
+ # list of conditions and the following disclaimer in the documentation and/or other
13
+ # materials provided with the distribution.
14
+ #
15
+ # Neither the name of the copyright holder nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without specific
17
+ # prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22
+ # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
29
+ # *********************************************************************************
30
+
31
+ require 'json'
32
+ require 'json-schema'
33
+
34
+ module URBANopt
35
+ module Reporting
36
+ module DefaultReports
37
+ ##
38
+ # Onsite solar PV system attributes
39
+ ##
40
+ class SolarPV
41
+ ##
42
+ # _Float_ - power capacity in kilowatts
43
+ #
44
+ attr_accessor :size_kw
45
+
46
+ ##
47
+ # Initialize SolarPV attributes from a hash. Solar PV attributes currently are limited to power capacity.
48
+ ##
49
+ # [parameters:]
50
+ #
51
+ # * +hash+ - _Hash_ - A hash containting a +:size_kw+ key/value pair which represents the nameplate capacity in kilowatts (kW)
52
+ #
53
+ def initialize(hash = {})
54
+ hash.delete_if { |k, v| v.nil? }
55
+
56
+ @size_kw = hash[:size_kw]
57
+ @id = hash[:id]
58
+
59
+ # initialize class variables @@validator and @@schema
60
+ @@validator ||= Validator.new
61
+ @@schema ||= @@validator.schema
62
+
63
+ # initialize @@logger
64
+ @@logger ||= URBANopt::Scenario::DefaultReports.logger
65
+ end
66
+
67
+ ##
68
+ # Convert to a Hash equivalent for JSON serialization
69
+ ##
70
+ def to_hash
71
+ result = {}
72
+
73
+ result[:size_kw] = @size_kw if @size_kw
74
+
75
+ return result
76
+ end
77
+
78
+ ##
79
+ # Merge PV systems
80
+ ##
81
+ def self.add_pv(existing_pv, new_pv)
82
+ if existing_pv.size_kw.nil? && new_pv.size_kw.nil?
83
+ existing_pv.size_kw = nil
84
+ else
85
+ existing_pv.size_kw = (existing_pv.size_kw || 0) + (new_pv.size_kw || 0)
86
+ end
87
+
88
+ return existing_pv
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,105 @@
1
+ # *********************************************************************************
2
+ # URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
3
+ # contributors. All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice, this list
9
+ # of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright notice, this
12
+ # list of conditions and the following disclaimer in the documentation and/or other
13
+ # materials provided with the distribution.
14
+ #
15
+ # Neither the name of the copyright holder nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without specific
17
+ # prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22
+ # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
29
+ # *********************************************************************************
30
+
31
+ require 'json'
32
+ require 'json-schema'
33
+
34
+ module URBANopt
35
+ module Reporting
36
+ module DefaultReports
37
+ ##
38
+ # Onsite storage system attributes
39
+ ##
40
+ class Storage
41
+ ##
42
+ # _Float_ - power capacity in kilowatts
43
+ #
44
+ attr_accessor :size_kw
45
+
46
+ ##
47
+ # _Float_ - storage capacity in kilowatt-hours
48
+ #
49
+ attr_accessor :size_kwh
50
+
51
+ ##
52
+ # Initialize Storage attributes from a hash. Storage attributes currently are limited to power and storage capacity.
53
+ ##
54
+ # [parameters:]
55
+ #
56
+ # * +hash+ - _Hash_ - A hash containting +:size_kw+ and +:size_kwh+ key/value pair which represents the power and storage capacity in kilowatts (kW) and kilowatt-hours respectively.
57
+ #
58
+ def initialize(hash = {})
59
+ hash.delete_if { |k, v| v.nil? }
60
+
61
+ @size_kw = hash[:size_kw]
62
+ @size_kwh = hash[:size_kwh]
63
+
64
+ # initialize class variables @@validator and @@schema
65
+ @@validator ||= Validator.new
66
+ @@schema ||= @@validator.schema
67
+
68
+ # initialize @@logger
69
+ @@logger ||= URBANopt::Scenario::DefaultReports.logger
70
+ end
71
+
72
+ ##
73
+ # Convert to a Hash equivalent for JSON serialization
74
+ ##
75
+ def to_hash
76
+ result = {}
77
+
78
+ result[:size_kw] = @size_kw if @size_kw
79
+ result[:size_kwh] = @size_kwh if @size_kwh
80
+
81
+ return result
82
+ end
83
+
84
+ ##
85
+ # Merge Storage systems
86
+ ##
87
+ def self.add_storage(existing_storage, new_storage)
88
+ if existing_storage.size_kw.nil?
89
+ existing_storage.size_kw = new_storage.size_kw
90
+ else
91
+ existing_storage.size_kw = (existing_storage.size_kw || 0) + (new_storage.size_kw || 0)
92
+ end
93
+
94
+ if existing_storage.size_kw.nil?
95
+ existing_storage.size_kwh = new_storage.size_kwh
96
+ else
97
+ existing_storage.size_kwh = (existing_storage.size_kwh || 0) + (new_storage.size_kwh || 0)
98
+ end
99
+
100
+ return existing_storage
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,300 @@
1
+ # *********************************************************************************
2
+ # URBANopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC, and other
3
+ # contributors. All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice, this list
9
+ # of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright notice, this
12
+ # list of conditions and the following disclaimer in the documentation and/or other
13
+ # materials provided with the distribution.
14
+ #
15
+ # Neither the name of the copyright holder nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without specific
17
+ # prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22
+ # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
23
+ # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
28
+ # OF THE POSSIBILITY OF SUCH DAMAGE.
29
+ # *********************************************************************************
30
+
31
+ require_relative 'validator'
32
+ require_relative 'logger'
33
+
34
+ require 'csv'
35
+ require 'pathname'
36
+ require 'json-schema'
37
+
38
+ module URBANopt
39
+ module Reporting
40
+ module DefaultReports
41
+ ##
42
+ # TimeseriesCSV include timesries reults reported in a CSV file.
43
+ ##
44
+ class TimeseriesCSV
45
+ attr_accessor :path, :first_report_datetime, :column_names #:nodoc:
46
+
47
+ ##
48
+ # TimeseriesCSV class initializes timeseries csv attributes: +:path+ , +:first_report_datetime+ , +:column_names+
49
+ ##
50
+ # +hash+ - _Hash_ - A hash which may contain a deserialized timeseries_csv.
51
+ ##
52
+ def initialize(hash = {})
53
+ hash.delete_if { |k, v| v.nil? }
54
+ hash = defaults.merge(hash)
55
+
56
+ @run_dir = ''
57
+
58
+ @path = hash[:path]
59
+ @first_report_datetime = hash[:first_report_datetime]
60
+
61
+ # from scenario csv shema get required reults to be aggregated
62
+ @required_column_names = load_scenario_csv_schema_headers
63
+
64
+ @column_names = hash[:column_names]
65
+ @column_names.delete_if { |x| !@required_column_names.include? x.split('(')[0] }
66
+
67
+ # hash of column_name to array of values, does not get serialized to hash
68
+ @mutex = Mutex.new
69
+ @data = nil
70
+
71
+ # initialize class variables @@validator and @@schema
72
+ @@validator ||= Validator.new
73
+ @@schema ||= @@validator.schema
74
+
75
+ # initialize @@logger
76
+ @@logger ||= URBANopt::Reporting::DefaultReports.logger
77
+ end
78
+
79
+ ##
80
+ # load required scenario report csv headers from reports schema
81
+ ##
82
+ def load_scenario_csv_schema_headers
83
+ # rubocop: disable Security/Open
84
+ scenario_csv_schema = open(File.expand_path('../default_reports/schema/scenario_csv_columns.txt', File.dirname(__FILE__)))
85
+ # rubocop: enable Security/Open
86
+
87
+ scenario_csv_schema_headers = []
88
+ File.readlines(scenario_csv_schema).each do |line|
89
+ l = line.delete("\n")
90
+ a = l.delete("\t")
91
+ r = a.delete("\r")
92
+ scenario_csv_schema_headers << r
93
+ end
94
+ return scenario_csv_schema_headers
95
+ end
96
+
97
+ ##
98
+ # Assigns default values if values does not exist.
99
+ ##
100
+ def defaults
101
+ hash = {}
102
+ hash[:path] = nil
103
+ hash[:column_names] = []
104
+ return hash
105
+ end
106
+
107
+ ##
108
+ # Gets run directory.
109
+ ##
110
+ # [parameters:]
111
+ # +name+ - _String_ - The name of the scenario (+directory_name+).
112
+ ##
113
+ def run_dir_name(name)
114
+ @run_dir = name
115
+ end
116
+
117
+ ##
118
+ # Converts to a Hash equivalent for JSON serialization.
119
+ ##
120
+ # - Exclude attributes with nil values.
121
+ # - Validate reporting_period hash properties against schema.
122
+ ##
123
+ def to_hash
124
+ result = {}
125
+ directory_path = Pathname.new File.expand_path(@run_dir.to_s, File.dirname(__FILE__)) if @run_dir
126
+ csv_path = Pathname.new @path if @path
127
+
128
+ relative_path = csv_path.to_s.sub(directory_path.to_s, '')
129
+
130
+ result[:path] = relative_path if @path
131
+ result[:first_report_datetime] = @first_report_datetime if @first_report_datetime
132
+ result[:column_names] = @column_names if @column_names
133
+
134
+ # validate timeseries_csv properties against schema
135
+ if @@validator.validate(@@schema[:definitions][:TimeseriesCSV][:properties], result).any?
136
+ raise "scenario_report properties does not match schema: #{@@validator.validate(@@schema[:definitions][:TimeseriesCSV][:properties], result)}"
137
+ end
138
+
139
+ return result
140
+ end
141
+
142
+ ##
143
+ # Reloads data from the CSV file.
144
+ ##
145
+ def reload_data(new_data)
146
+ @mutex.synchronize do
147
+ @data = {}
148
+ @column_names = []
149
+ new_data.each do |row|
150
+ if @column_names.empty?
151
+ @column_names = row
152
+ @column_names.each do |column_name|
153
+ @data[column_name] = []
154
+ end
155
+ else
156
+ row.each_with_index do |value, i|
157
+ if i == 0
158
+ @data[@column_names[i]] << value
159
+ else
160
+ @data[@column_names[i]] << value.to_f
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ ##
169
+ # Loads data from the CSV file.
170
+ ##
171
+ def load_data
172
+ @mutex.synchronize do
173
+ if @data.nil?
174
+ @data = {}
175
+ @column_names = []
176
+ CSV.foreach(@path) do |row|
177
+ if @column_names.empty?
178
+ @column_names = row
179
+ @column_names.each do |column_name|
180
+ @data[column_name] = []
181
+ end
182
+ else
183
+ row.each_with_index do |value, i|
184
+ @data[@column_names[i]] << value
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ ##
193
+ # Gets data for each column name in the CSV file.
194
+ ##
195
+ # [parameters:]
196
+ # +column_name+ - _String_ - The header of each column in the CSV file.
197
+ ##
198
+ def get_data(column_name)
199
+ load_data
200
+ return @data[column_name]
201
+ end
202
+
203
+ ##
204
+ # Saves data to the the scenario report CSV file.
205
+ ##
206
+ # [parameters:]
207
+ # +path+ - _String_ - The path of the scenario report CSV (default_scenario_report.csv).
208
+ ##
209
+ def save_data(path = nil)
210
+ if path.nil?
211
+ path = @path
212
+ end
213
+
214
+ File.open(path, 'w') do |f|
215
+ f.puts @column_names.join(',')
216
+ n = @data[@column_names[0]].size - 1
217
+
218
+ (0..n).each do |i|
219
+ line = []
220
+ @column_names.each do |column_name|
221
+ line << @data[column_name][i]
222
+ end
223
+ f.puts line.join(',')
224
+ end
225
+ begin
226
+ f.fsync
227
+ rescue StandardError
228
+ f.flush
229
+ end
230
+ end
231
+ end
232
+
233
+ ##
234
+ # Merges timeseries csv to each other.
235
+ ##
236
+ # - initialize first_report_datetime with the incoming first_report_datetime if its nil.
237
+ # - checks if first_report_datetime are identical.
238
+ # - merge the column names
239
+ # - merge the column data
240
+ ##
241
+ # [parameters:]
242
+ # +other+ - _TimeseriesCSV_ - An object of TimeseriesCSV class.
243
+ ##
244
+ def add_timeseries_csv(other)
245
+ # initialize first_report_datetime with the incoming first_report_datetime if its nil.
246
+ if @first_report_datetime.nil? || @first_report_datetime == ''
247
+ @first_report_datetime = other.first_report_datetime
248
+ end
249
+
250
+ # checks if first_report_datetime are identical.
251
+ if @first_report_datetime != other.first_report_datetime
252
+ raise "first_report_datetime '#{@first_report_datetime}' does not match other.first_report_datetime '#{other.first_report_datetime}'"
253
+ end
254
+
255
+ # merge the column names
256
+ other_column_names = []
257
+ other.column_names.each do |n|
258
+ if !n[0, 4].casecmp('ZONE').zero?
259
+ other_column_names << n
260
+ end
261
+ end
262
+
263
+ @column_names = @column_names.concat(other_column_names).uniq
264
+
265
+ # merge the column data
266
+ other.column_names.each do |column_name|
267
+ if !column_name[0, 4].casecmp('ZONE').zero?
268
+ if !@column_names.include? column_name
269
+ @column_names.push column_name
270
+ end
271
+
272
+ new_values = other.get_data(column_name)
273
+
274
+ if @data.nil?
275
+ @data = {}
276
+ end
277
+
278
+ current_values = @data[column_name]
279
+
280
+ if current_values
281
+ if current_values.size != new_values.size
282
+ raise 'Values of different sizes in add_timeseries_csv'
283
+ end
284
+ new_values.each_with_index do |value, i|
285
+ # aggregate all columns except Datime column
286
+ if column_name != 'Datetime'
287
+ new_values[i] = value.to_f + current_values[i].to_f
288
+ end
289
+ end
290
+ @data[column_name] = new_values
291
+ else
292
+ @data[column_name] = new_values
293
+ end
294
+ end
295
+ end
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end