urbanopt-reporting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,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 'end_uses'
32
+ require_relative 'end_use'
33
+ require_relative 'date'
34
+ require_relative 'validator'
35
+
36
+ require 'json'
37
+ require 'json-schema'
38
+
39
+ module URBANopt
40
+ module Reporting
41
+ module DefaultReports
42
+ ##
43
+ # ReportingPeriod includes all the results of a specific reporting period.
44
+ ##
45
+ class ReportingPeriod
46
+ attr_accessor :id, :name, :multiplier, :start_date, :end_date, :month, :day_of_month, :year, :total_site_energy, :total_source_energy,
47
+ :net_site_energy, :net_source_energy, :total_utility_cost, :net_utility_cost, :utility_costs, :electricity, :natural_gas, :additional_fuel, :district_cooling,
48
+ :district_heating, :water, :electricity_produced, :end_uses, :energy_production, :photovoltaic,
49
+ :fuel_type, :total_cost, :usage_cost, :demand_cost, :comfort_result, :time_setpoint_not_met_during_occupied_cooling,
50
+ :time_setpoint_not_met_during_occupied_heating, :time_setpoint_not_met_during_occupied_hours, :hours_out_of_comfort_bounds_PMV, :hours_out_of_comfort_bounds_PPD #:nodoc:
51
+ # ReportingPeriod class initializes the reporting period attributes:
52
+ # +:id+ , +:name+ , +:multiplier+ , +:start_date+ , +:end_date+ , +:month+ , +:day_of_month+ , +:year+ , +:total_site_energy+ , +:total_source_energy+ ,
53
+ # +:net_site_energy+ , +:net_source_energy+ , +:total_utility_cost , +:net_utility_cost+ , +:utility_costs+ , +:electricity+ , +:natural_gas+ , +:additional_fuel+ , +:district_cooling+ ,
54
+ # +:district_heating+ , +:water+ , +:electricity_produced+ , +:end_uses+ , +:energy_production+ , +:photovoltaic+ ,
55
+ # +:fuel_type+ , +:total_cost+ , +:usage_cost+ , +:demand_cost+ , +:comfort_result+ , +:time_setpoint_not_met_during_occupied_cooling+ ,
56
+ # +:time_setpoint_not_met_during_occupied_heating+ , +:time_setpoint_not_met_during_occupied_hours+
57
+ ##
58
+ # [parameters:]
59
+ # +hash+ - _Hash_ - A hash which may contain a deserialized reporting_period.
60
+ ##
61
+ def initialize(hash = {})
62
+ hash.delete_if { |k, v| v.nil? }
63
+ hash = defaults.merge(hash)
64
+
65
+ @id = hash[:id]
66
+ @name = hash[:name]
67
+ @multiplier = hash[:multiplier]
68
+ @start_date = Date.new(hash[:start_date])
69
+ @end_date = Date.new(hash[:end_date])
70
+
71
+ @total_site_energy = hash[:total_site_energy]
72
+ @total_source_energy = hash[:total_source_energy]
73
+ @net_site_energy = hash [:net_site_energy]
74
+ @net_source_energy = hash [:net_source_energy]
75
+ @net_utility_cost = hash [:net_utility_cost]
76
+ @total_utility_cost = hash [:total_utility_cost]
77
+ @electricity = hash [:electricity]
78
+ @natural_gas = hash [:natural_gas]
79
+ @additional_fuel = hash [:additional_fuel]
80
+ @district_cooling = hash [:district_cooling]
81
+ @district_heating = hash[:district_heating]
82
+ @water = hash[:water]
83
+ @electricity_produced = hash[:electricity_produced]
84
+ @end_uses = EndUses.new(hash[:end_uses])
85
+
86
+ @energy_production = hash[:energy_production]
87
+
88
+ @utility_costs = hash[:utility_costs]
89
+
90
+ @comfort_result = hash[:comfort_result]
91
+
92
+ # initialize class variables @@validator and @@schema
93
+ @@validator ||= Validator.new
94
+ @@schema ||= @@validator.schema
95
+ end
96
+
97
+ ##
98
+ # Assigns default values if values do not exist.
99
+ ##
100
+ def defaults
101
+ hash = {}
102
+
103
+ hash[:id] = nil
104
+ hash[:name] = nil
105
+ hash[:multiplier] = nil
106
+ hash[:start_date] = Date.new.to_hash
107
+ hash[:end_date] = Date.new.to_hash
108
+
109
+ hash[:total_site_energy] = nil
110
+ hash[:total_source_energy] = nil
111
+ hash [:net_site_energy] = nil
112
+ hash [:net_source_energy] = nil
113
+ hash [:net_utility_cost] = nil
114
+ hash [:total_utility_cost] = nil
115
+ hash [:electricity] = nil
116
+ hash [:natural_gas] = nil
117
+ hash [:additional_fuel] = nil
118
+ hash [:district_cooling] = nil
119
+ hash[:district_heating] = nil
120
+
121
+ hash[:electricity_produced] = nil
122
+ hash[:end_uses] = EndUses.new.to_hash
123
+ hash[:energy_production] = { electricity_produced: { photovoltaic: nil } }
124
+ hash[:utility_costs] = [{ fuel_type: nil, total_cost: nil, usage_cost: nil, demand_cost: nil }]
125
+ hash[:comfort_result] = { time_setpoint_not_met_during_occupied_cooling: nil, time_setpoint_not_met_during_occupied_heating: nil,
126
+ time_setpoint_not_met_during_occupied_hours: nil, hours_out_of_comfort_bounds_PMV: nil, hours_out_of_comfort_bounds_PPD: nil }
127
+
128
+ return hash
129
+ end
130
+
131
+ ##
132
+ # Converts to a Hash equivalent for JSON serialization.
133
+ ##
134
+ # - Exclude attributes with nil values.
135
+ # - Validate reporting_period hash properties against schema.
136
+ #
137
+ def to_hash
138
+ result = {}
139
+
140
+ result[:id] = @id if @id
141
+ result[:name] = @name if @name
142
+ result[:multiplier] = @multiplier if @multiplier
143
+ result[:start_date] = @start_date.to_hash if @start_date
144
+ result[:end_date] = @end_date.to_hash if @end_date
145
+ result[:total_site_energy] = @total_site_energy if @total_site_energy
146
+ result[:total_source_energy] = @total_source_energy if @total_source_energy
147
+ result[:net_site_energy] = @net_site_energy if @net_site_energy
148
+ result[:net_source_energy] = @net_source_energy if @net_source_energy
149
+ result[:net_utility_cost] = @net_utility_cost if @net_utility_cost
150
+ result[:total_utility_cost] = @total_utility_cost if @total_utility_cost
151
+ result[:electricity] = @electricity if @electricity
152
+ result[:natural_gas] = @natural_gas if @natural_gas
153
+ result[:additional_fuel] = @additional_fuel if @additional_fuel
154
+ result[:district_cooling] = @district_cooling if @district_cooling
155
+ result[:district_heating] = @district_heating if @district_heating
156
+ result[:water] = @water if @water
157
+ result[:electricity_produced] = @electricity_produced if @electricity_produced
158
+ result[:end_uses] = @end_uses.to_hash if @end_uses
159
+
160
+ energy_production_hash = @energy_production if @energy_production
161
+ energy_production_hash.delete_if { |k, v| v.nil? }
162
+ energy_production_hash.each do |eph|
163
+ eph.delete_if { |k, v| v.nil? }
164
+ end
165
+
166
+ result[:energy_production] = energy_production_hash if @energy_production
167
+
168
+ if @utility_costs.any?
169
+ result[:utility_costs] = @utility_costs
170
+ @utility_costs.each do |uc|
171
+ uc&.delete_if { |k, v| v.nil? }
172
+ end
173
+ end
174
+
175
+ comfort_result_hash = @comfort_result if @comfort_result
176
+ comfort_result_hash.delete_if { |k, v| v.nil? }
177
+ result[:comfort_result] = comfort_result_hash if @comfort_result
178
+
179
+ # validates +reporting_period+ properties against schema for reporting period.
180
+ if @@validator.validate(@@schema[:definitions][:ReportingPeriod][:properties], result).any?
181
+ raise "feature_report properties does not match schema: #{@@validator.validate(@@schema[:definitions][:ReportingPeriod][:properties], result)}"
182
+ end
183
+
184
+ return result
185
+ end
186
+
187
+ ##
188
+ # Adds up +existing_value+ and +new_values+ if not nill.
189
+ ##
190
+ # [parameter:]
191
+ # +existing_value+ - _Float_ - A value corresponding to a ReportingPeriod attribute.
192
+ ##
193
+ # +new_value+ - _Float_ - A value corresponding to a ReportingPeriod attribute.
194
+ ##
195
+ def self.add_values(existing_value, new_value)
196
+ if existing_value && new_value
197
+ existing_value += new_value
198
+ elsif new_value
199
+ existing_value = new_value
200
+ end
201
+ return existing_value
202
+ end
203
+
204
+ ##
205
+ # Merges an +existing_period+ with a +new_period+ if not nil.
206
+ ##
207
+ # [Parameters:]
208
+ # +existing_period+ - _ReportingPeriod_ - An object of ReportingPeriod class.
209
+ ##
210
+ # +new_period+ - _ReportingPeriod_ - An object of ReportingPeriod class.
211
+ ##
212
+ def self.merge_reporting_period(existing_period, new_period)
213
+ # modify the existing_period by summing up the results
214
+ existing_period.total_site_energy = add_values(existing_period.total_site_energy, new_period.total_site_energy)
215
+ existing_period.total_source_energy = add_values(existing_period.total_source_energy, new_period.total_source_energy)
216
+ existing_period.net_source_energy = add_values(existing_period.net_source_energy, new_period.net_source_energy)
217
+ existing_period.net_utility_cost = add_values(existing_period.net_utility_cost, new_period.net_utility_cost)
218
+ existing_period.total_utility_cost = add_values(existing_period.total_utility_cost, new_period.total_utility_cost)
219
+ existing_period.electricity = add_values(existing_period.electricity, new_period.electricity)
220
+ existing_period.natural_gas = add_values(existing_period.natural_gas, new_period.natural_gas)
221
+ existing_period.additional_fuel = add_values(existing_period.additional_fuel, new_period.additional_fuel)
222
+ existing_period.district_cooling = add_values(existing_period.district_cooling, new_period.district_cooling)
223
+ existing_period.district_heating = add_values(existing_period.district_heating, new_period.district_heating)
224
+ existing_period.water = add_values(existing_period.water, new_period.water)
225
+ existing_period.electricity_produced = add_values(existing_period.electricity_produced, new_period.electricity_produced)
226
+
227
+ # merge end uses
228
+ new_end_uses = new_period.end_uses
229
+ existing_period.end_uses&.merge_end_uses!(new_end_uses)
230
+
231
+ if existing_period.energy_production
232
+ if existing_period.energy_production[:electricity_produced]
233
+ existing_period.energy_production[:electricity_produced][:photovoltaic] = add_values(existing_period.energy_production[:electricity_produced][:photovoltaic], new_period.energy_production[:electricity_produced][:photovoltaic])
234
+ end
235
+ end
236
+
237
+ existing_period.utility_costs&.each_with_index do |item, i|
238
+ existing_period.utility_costs[i][:fuel_type] = existing_period.utility_costs[i][:fuel_type]
239
+ existing_period.utility_costs[i][:total_cost] = add_values(existing_period.utility_costs[i][:total_cost], new_period.utility_costs[i][:total_cost])
240
+ existing_period.utility_costs[i][:usage_cost] = add_values(existing_period.utility_costs[i][:usage_cost], new_period.utility_costs[i][:usage_cost])
241
+ existing_period.utility_costs[i][:demand_cost] = add_values(existing_period.utility_costs[i][:demand_cost], new_period.utility_costs[i][:demand_cost])
242
+ end
243
+
244
+ if existing_period.comfort_result
245
+ existing_period.comfort_result[:time_setpoint_not_met_during_occupied_cooling] = add_values(existing_period.comfort_result[:time_setpoint_not_met_during_occupied_cooling], new_period.comfort_result[:time_setpoint_not_met_during_occupied_cooling])
246
+ existing_period.comfort_result[:time_setpoint_not_met_during_occupied_heating] = add_values(existing_period.comfort_result[:time_setpoint_not_met_during_occupied_heating], new_period.comfort_result[:time_setpoint_not_met_during_occupied_heating])
247
+ existing_period.comfort_result[:time_setpoint_not_met_during_occupied_hours] = add_values(existing_period.comfort_result[:time_setpoint_not_met_during_occupied_hours], new_period.comfort_result[:time_setpoint_not_met_during_occupied_hours])
248
+ existing_period.comfort_result[:hours_out_of_comfort_bounds_PMV] = add_values(existing_period.comfort_result[:hours_out_of_comfort_bounds_PMV], new_period.comfort_result[:hours_out_of_comfort_bounds_PMV])
249
+ existing_period.comfort_result[:hours_out_of_comfort_bounds_PPD] = add_values(existing_period.comfort_result[:hours_out_of_comfort_bounds_PPD], new_period.comfort_result[:hours_out_of_comfort_bounds_PPD])
250
+ end
251
+
252
+ return existing_period
253
+ end
254
+
255
+ ##
256
+ # Merges multiple reporting periods together.
257
+ # - If +existing_periods+ and +new_periods+ ids are equal,
258
+ # modify the existing_periods by merging the new periods results
259
+ # - If existing periods are empty, initialize with new_periods.
260
+ # - Raise an error if the existing periods are not identical with new periods (cannot have different reporting period ids).
261
+ ##
262
+ # [parameters:]
263
+ ##
264
+ # +existing_periods+ - _Array_ - An array of ReportingPeriod objects.
265
+ ##
266
+ # +new_periods+ - _Array_ - An array of ReportingPeriod objects.
267
+ ##
268
+ def self.merge_reporting_periods(existing_periods, new_periods)
269
+ id_list_existing = []
270
+ id_list_new = []
271
+ id_list_existing = existing_periods.collect(&:id)
272
+ id_list_new = new_periods.collect(&:id)
273
+
274
+ if id_list_existing == id_list_new
275
+
276
+ existing_periods.each_index do |index|
277
+ # if +existing_periods+ and +new_periods+ ids are equal,
278
+ # modify the existing_periods by merging the new periods results
279
+ existing_periods[index] = merge_reporting_period(existing_periods[index], new_periods[index])
280
+ end
281
+
282
+ elsif existing_periods.empty?
283
+
284
+ # if existing periods are empty, initialize with new_periods
285
+ # the = operator would link existing_periods and new_periods to the same object in memory
286
+ # we want to initialize with a deep clone of new_periods
287
+ existing_periods = Marshal.load(Marshal.dump(new_periods))
288
+
289
+ else
290
+ # raise an error if the existing periods are not identical with new periods (cannot have different reporting period ids)
291
+ raise 'cannot merge different reporting periods'
292
+
293
+ end
294
+
295
+ return existing_periods
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,317 @@
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 'construction_cost'
32
+ require_relative 'feature_report'
33
+ require_relative 'logger'
34
+ require_relative 'program'
35
+ require_relative 'reporting_period'
36
+ require_relative 'timeseries_csv'
37
+ require_relative 'distributed_generation'
38
+ require_relative 'validator'
39
+
40
+ require 'json'
41
+ require 'json-schema'
42
+ require 'pathname'
43
+
44
+ module URBANopt
45
+ module Reporting
46
+ module DefaultReports
47
+ ##
48
+ # ScenarioReport can generate two types of reports from a scenario.
49
+ # The first is a JSON format saved to 'default_scenario_report.json'.
50
+ # The second is a CSV format saved to 'default_scenario_report.csv'.
51
+ ##
52
+ class ScenarioReport
53
+ attr_accessor :id, :name, :directory_name, :timesteps_per_hour, :number_of_not_started_simulations,
54
+ :number_of_started_simulations, :number_of_complete_simulations, :number_of_failed_simulations,
55
+ :timeseries_csv, :location, :program, :construction_costs, :reporting_periods, :feature_reports, :distributed_generation # :nodoc:
56
+ # ScenarioReport class intializes the scenario report attributes:
57
+ # +:id+ , +:name+ , +:directory_name+, +:timesteps_per_hour+ , +:number_of_not_started_simulations+ ,
58
+ # +:number_of_started_simulations+ , +:number_of_complete_simulations+ , +:number_of_failed_simulations+ ,
59
+ # +:timeseries_csv+ , +:location+ , +:program+ , +:construction_costs+ , +:reporting_periods+ , +:feature_reports+
60
+ ##
61
+ # Each ScenarioReport object corresponds to a single Scenario.
62
+ ##
63
+ # [parameters:]
64
+ # +hash+ - _Hash_ - A hash of a previously serialized ScenarioReport.
65
+ ##
66
+ def initialize(hash = {})
67
+ hash.delete_if { |k, v| v.nil? }
68
+ hash = defaults.merge(hash)
69
+
70
+ @id = hash[:id]
71
+ @name = hash[:name]
72
+ @directory_name = hash[:directory_name]
73
+ @timesteps_per_hour = hash[:timesteps_per_hour]
74
+ @number_of_not_started_simulations = hash[:number_of_not_started_simulations]
75
+ @number_of_started_simulations = hash[:number_of_started_simulations]
76
+ @number_of_complete_simulations = hash[:number_of_complete_simulations]
77
+ @number_of_failed_simulations = hash[:number_of_failed_simulations]
78
+ @timeseries_csv = TimeseriesCSV.new(hash[:timeseries_csv])
79
+ @location = Location.new(hash[:location])
80
+ @program = Program.new(hash[:program])
81
+ @distributed_generation = DistributedGeneration.new(hash[:distributed_generation] || {})
82
+
83
+ @construction_costs = []
84
+ hash[:construction_costs].each do |cc|
85
+ @constructiion_costs << ConstructionCost.new(cc)
86
+ end
87
+
88
+ @reporting_periods = []
89
+ hash[:reporting_periods].each do |rp|
90
+ @reporting_periods << ReportingPeriod.new(rp)
91
+ end
92
+
93
+ # feature_report is intialized here to be used in the add_feature_report method
94
+ @feature_reports = []
95
+ hash[:feature_reports].each do |fr|
96
+ @feature_reports << FeatureReport.new(fr)
97
+ end
98
+
99
+ @file_name = 'default_scenario_report'
100
+
101
+ # initialize class variables @@validator and @@schema
102
+ @@validator ||= Validator.new
103
+ @@schema ||= @@validator.schema
104
+
105
+ # initialize @@logger
106
+ @@logger ||= URBANopt::Reporting::DefaultReports.logger
107
+ end
108
+
109
+ ##
110
+ # Assigns default values if values do not exist.
111
+ ##
112
+ def defaults
113
+ hash = {}
114
+ hash[:id] = nil.to_s
115
+ hash[:name] = nil.to_s
116
+ hash[:directory_name] = nil.to_s
117
+ hash[:timesteps_per_hour] = nil # unknown
118
+ hash[:number_of_not_started_simulations] = 0
119
+ hash[:number_of_started_simulations] = 0
120
+ hash[:number_of_complete_simulations] = 0
121
+ hash[:number_of_failed_simulations] = 0
122
+ hash[:timeseries_csv] = TimeseriesCSV.new.to_hash
123
+ hash[:location] = Location.new.defaults
124
+ hash[:program] = Program.new.to_hash
125
+ hash[:construction_costs] = []
126
+ hash[:reporting_periods] = []
127
+ hash[:feature_reports] = []
128
+ return hash
129
+ end
130
+
131
+ ##
132
+ # Gets the saved JSON file path.
133
+ ##
134
+ def json_path
135
+ File.join(@directory_name, @file_name + '.json')
136
+ end
137
+
138
+ ##
139
+ # Gets the saved CSV file path.
140
+ ##
141
+ def csv_path
142
+ File.join(@directory_name, @file_name + '.csv')
143
+ end
144
+
145
+ ##
146
+ # Saves the 'default_scenario_report.json' and 'default_scenario_report.csv' files
147
+ ##
148
+ # [parameters]:
149
+ # +file_name+ - _String_ - Assign a name to the saved scenario results file without an extension
150
+ def save(file_name = 'default_scenario_report')
151
+ # reassign the initialize local variable @file_name to the file name input.
152
+ @file_name = file_name
153
+
154
+ # save the scenario reports csv and json data
155
+ old_timeseries_path = nil
156
+ if !@timeseries_csv.path.nil?
157
+ old_timeseries_path = @timeseries_csv.path
158
+ end
159
+
160
+ @timeseries_csv.path = File.join(@directory_name, file_name + '.csv')
161
+ @timeseries_csv.save_data
162
+
163
+ hash = {}
164
+ hash[:scenario_report] = to_hash
165
+ hash[:feature_reports] = []
166
+ @feature_reports.each do |feature_report|
167
+ hash[:feature_reports] << feature_report.to_hash
168
+ end
169
+
170
+ json_name_path = File.join(@directory_name, file_name + '.json')
171
+
172
+ File.open(json_name_path, 'w') do |f|
173
+ f.puts JSON.pretty_generate(hash)
174
+ # make sure data is written to the disk one way or the other
175
+ begin
176
+ f.fsync
177
+ rescue StandardError
178
+ f.flush
179
+ end
180
+ end
181
+
182
+ if !old_timeseries_path.nil?
183
+ @timeseries_csv.path = old_timeseries_path
184
+ else
185
+ @timeseries_csv.path = File.join(@directory_name, file_name + '.csv')
186
+ end
187
+
188
+ # save the feature reports csv and json data
189
+ # @feature_reports.each do |feature_report|
190
+ # feature_report.save_feature_report()
191
+ # end
192
+
193
+ return true
194
+ end
195
+
196
+ ##
197
+ # Converts to a Hash equivalent for JSON serialization.
198
+ ##
199
+ # - Exclude attributes with nil values.
200
+ # - Validate reporting_period hash properties against schema.
201
+ ##
202
+ def to_hash
203
+ result = {}
204
+ result[:id] = @id if @id
205
+ result[:name] = @name if @name
206
+ result[:directory_name] = @directory_name if @directory_name
207
+ result[:timesteps_per_hour] = @timesteps_per_hour if @timesteps_per_hour
208
+ result[:number_of_not_started_simulations] = @number_of_not_started_simulations if @number_of_not_started_simulations
209
+ result[:number_of_started_simulations] = @number_of_started_simulations if @number_of_started_simulations
210
+ result[:number_of_complete_simulations] = @number_of_complete_simulations if @number_of_complete_simulations
211
+ result[:number_of_failed_simulations] = @number_of_failed_simulations if @number_of_failed_simulations
212
+ result[:timeseries_csv] = @timeseries_csv.to_hash if @timeseries_csv
213
+ result[:location] = @location.to_hash if @location
214
+ result[:program] = @program.to_hash if @program
215
+ result[:distributed_generation] = @distributed_generation.to_hash if @distributed_generation
216
+
217
+ result[:construction_costs] = []
218
+ @construction_costs&.each { |cc| result[:construction_costs] << cc.to_hash }
219
+
220
+ result[:reporting_periods] = []
221
+ @reporting_periods&.each { |rp| result[:reporting_periods] << rp.to_hash }
222
+
223
+ # result[:feature_reports] = []
224
+ # @feature_reports.each { |fr| result[:feature_reports] << fr.to_hash } if @feature_reports
225
+
226
+ # validate scenario_report properties against schema
227
+ if @@validator.validate(@@schema[:definitions][:ScenarioReport][:properties], result).any?
228
+ raise "scenario_report properties does not match schema: #{@@validator.validate(@@schema[:definitions][:ScenarioReport][:properties], result)}"
229
+ end
230
+
231
+ # have to use the module method because we have not yet initialized the class one
232
+ @@logger.info("Scenario name: #{@name}")
233
+
234
+ return result
235
+ end
236
+
237
+ ##
238
+ # Add feature reports to each other.
239
+ ##
240
+ # - check if a feature report have been already added.
241
+ # - check feature simulation status
242
+ # - merge timeseries_csv information
243
+ # - merge program information
244
+ # - merge construction_cost information
245
+ # - merge reporting_periods information
246
+ # - add the array of feature_reports
247
+ # - scenario report location takes the location of the first feature in the list
248
+ ##
249
+ # [parmeters:]
250
+ # +feature_report+ - _FeatureReport_ - An object of FeatureReport class.
251
+ ##
252
+ def add_feature_report(feature_report)
253
+ # check if the timesteps_per_hour are identical
254
+ if @timesteps_per_hour.nil? || @timesteps_per_hour == ''
255
+ @timesteps_per_hour = feature_report.timesteps_per_hour
256
+ else
257
+ if feature_report.timesteps_per_hour.is_a?(Integer) && feature_report.timesteps_per_hour != @timesteps_per_hour
258
+ raise "FeatureReport timesteps_per_hour = '#{feature_report.timesteps_per_hour}' does not match scenario timesteps_per_hour '#{@timesteps_per_hour}'"
259
+ end
260
+ end
261
+
262
+ # check if first report_report_datetime are identical.
263
+ if @timeseries_csv.first_report_datetime.nil? || @timeseries_csv.first_report_datetime == ''
264
+ @timeseries_csv.first_report_datetime = feature_report.timeseries_csv.first_report_datetime
265
+ else
266
+ if feature_report.timeseries_csv.first_report_datetime != @timeseries_csv.first_report_datetime
267
+ raise "first_report_datetime '#{@first_report_datetime}' does not match other.first_report_datetime '#{feature_report.timeseries_csv.first_report_datetime}'"
268
+ end
269
+ end
270
+
271
+ # check that we have not already added this feature
272
+ id = feature_report.id
273
+ @feature_reports.each do |existing_feature_report|
274
+ if existing_feature_report.id == id
275
+ raise "FeatureReport with id = '#{id}' has already been added"
276
+ end
277
+ end
278
+
279
+ # check feature simulation status
280
+ if feature_report.simulation_status == 'Not Started'
281
+ @number_of_not_started_simulations += 1
282
+ elsif feature_report.simulation_status == 'Started'
283
+ @number_of_started_simulations += 1
284
+ elsif feature_report.simulation_status == 'Complete'
285
+ @number_of_complete_simulations += 1
286
+ elsif feature_report.simulation_status == 'Failed'
287
+ @number_of_failed_simulations += 1
288
+ else
289
+ raise "Unknown feature_report simulation_status = '#{feature_report.simulation_status}'"
290
+ end
291
+
292
+ # merge timeseries_csv information
293
+ @timeseries_csv.add_timeseries_csv(feature_report.timeseries_csv)
294
+
295
+ @timeseries_csv.run_dir_name(@directory_name)
296
+
297
+ # merge program information
298
+ @program.add_program(feature_report.program)
299
+
300
+ # merge construction_cost information
301
+ @construction_costs = ConstructionCost.merge_construction_costs(@construction_costs, feature_report.construction_costs)
302
+
303
+ # merge reporting_periods information
304
+ @reporting_periods = ReportingPeriod.merge_reporting_periods(@reporting_periods, feature_report.reporting_periods)
305
+
306
+ @distributed_generation = DistributedGeneration.merge_distributed_generation(@distributed_generation, feature_report.distributed_generation)
307
+
308
+ # add feature_report
309
+ @feature_reports << feature_report
310
+
311
+ # scenario report location takes the location of the first feature in the list
312
+ @location = feature_reports[0].location
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end