urbanopt-scenario 0.1.1 → 0.2.0.pre1

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -8
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +2 -10
  4. data/.github/pull_request_template.md +5 -15
  5. data/{.github/CONTRIBUTING.md → CONTRIBUTING.md} +0 -0
  6. data/Gemfile +4 -2
  7. data/Jenkinsfile +2 -2
  8. data/Rakefile +1 -1
  9. data/docs/package-lock.json +4607 -6451
  10. data/docs/package.json +1 -1
  11. data/lib/measures/default_feature_reports/LICENSE.md +1 -1
  12. data/lib/measures/default_feature_reports/README.md +1 -1
  13. data/lib/measures/default_feature_reports/measure.rb +256 -41
  14. data/lib/measures/default_feature_reports/measure.xml +19 -15
  15. data/lib/urbanopt/scenario/default_reports/distributed_generation.rb +204 -17
  16. data/lib/urbanopt/scenario/default_reports/feature_report.rb +47 -0
  17. data/lib/urbanopt/scenario/default_reports/program.rb +6 -1
  18. data/lib/urbanopt/scenario/default_reports/reporting_period.rb +5 -2
  19. data/lib/urbanopt/scenario/default_reports/scenario_report.rb +17 -6
  20. data/lib/urbanopt/scenario/default_reports/schema/README.md +11 -12
  21. data/lib/urbanopt/scenario/default_reports/schema/scenario_csv_columns.txt +14 -6
  22. data/lib/urbanopt/scenario/default_reports/schema/scenario_schema.json +27 -18
  23. data/lib/urbanopt/scenario/default_reports/solar_pv.rb +1 -0
  24. data/lib/urbanopt/scenario/default_reports/timeseries_csv.rb +29 -3
  25. data/lib/urbanopt/scenario/scenario_runner_osw.rb +21 -5
  26. data/lib/urbanopt/scenario/version.rb +1 -1
  27. data/urbanopt-scenario-gem.gemspec +7 -5
  28. metadata +39 -25
  29. data/.travis.yml +0 -23
@@ -73,25 +73,45 @@ module URBANopt
73
73
  attr_accessor :total_energy_cost_us_dollars
74
74
 
75
75
  ##
76
- # _SolarPV_ - Installed \solar PV attributes
76
+ # _Array_ - List of _SolarPV_ systems
77
77
  #
78
78
  attr_accessor :solar_pv
79
79
 
80
80
  ##
81
- # _Wind_ - Installed \wind attributes
81
+ # _Array_ - List of _Wind_ systems
82
82
  #
83
83
  attr_accessor :wind
84
84
 
85
85
  ##
86
- # _Generator_ - Installed \generator attributes
86
+ # _Array_ - List of _Generator_ systems
87
87
  #
88
88
  attr_accessor :generator
89
89
 
90
90
  ##
91
- # _Storage_ - Installed \storage attributes
91
+ # _Array_ - List of _Storage_ systems
92
92
  #
93
93
  attr_accessor :storage
94
94
 
95
+ ##
96
+ # _Float_ - Installed solar PV capacity
97
+ #
98
+ attr_accessor :total_solar_pv_kw
99
+
100
+ ##
101
+ # _Float_ - Installed wind capacity
102
+ #
103
+ attr_accessor :total_wind_kw
104
+
105
+ ##
106
+ # _Float_ - Installed storage capacity
107
+ #
108
+ attr_accessor :total_storage_kw
109
+
110
+ ##
111
+ # _Float_ - Installed generator capacity
112
+ #
113
+ attr_accessor :total_generator_kw
114
+
95
115
  ##
96
116
  # Initialize distributed generation system design and financial metrics.
97
117
  #
@@ -113,10 +133,85 @@ module URBANopt
113
133
  @year_one_bill_us_dollars = hash[:year_one_bill_us_dollars]
114
134
  @total_energy_cost_us_dollars = hash[:total_energy_cost_us_dollars]
115
135
 
116
- @solar_pv = SolarPV.new(hash[:solar_pv] || {})
117
- @wind = Wind.new(hash[:wind] || {})
118
- @generator = Generator.new(hash[:generator] || {})
119
- @storage = Storage.new(hash[:storage] || {})
136
+ @total_solar_pv_kw = nil
137
+ @total_wind_kw = nil
138
+ @total_generator_kw = nil
139
+ @total_storage_kw = nil
140
+ @total_storage_kwh = nil
141
+
142
+ @solar_pv = []
143
+ if hash[:solar_pv].class == Hash
144
+ hash[:solar_pv] = [hash[:solar_pv]]
145
+ elsif hash[:solar_pv].nil?
146
+ hash[:solar_pv] = []
147
+ end
148
+
149
+ hash[:solar_pv].each do |s|
150
+ if !s[:size_kw].nil? && (s[:size_kw] != 0)
151
+ @solar_pv.push SolarPV.new(s)
152
+ if @total_solar_pv_kw.nil?
153
+ @total_solar_pv_kw = @solar_pv[-1].size_kw
154
+ else
155
+ @total_solar_pv_kw += @solar_pv[-1].size_kw
156
+ end
157
+ end
158
+ end
159
+
160
+ @wind = []
161
+ if hash[:wind].class == Hash
162
+ hash[:wind] = [hash[:wind]]
163
+ elsif hash[:wind].nil?
164
+ hash[:wind] = []
165
+ end
166
+
167
+ hash[:wind].each do |s|
168
+ if !s[:size_kw].nil? && (s[:size_kw] != 0)
169
+ @wind.push Wind.new(s)
170
+ if @total_wind_kw.nil?
171
+ @total_wind_kw = @wind[-1].size_kw
172
+ else
173
+ @total_wind_kw += @wind[-1].size_kw
174
+ end
175
+ end
176
+ end
177
+
178
+ @generator = []
179
+ if hash[:generator].class == Hash
180
+ hash[:generator] = [hash[:generator]]
181
+ elsif hash[:generator].nil?
182
+ hash[:generator] = []
183
+ end
184
+
185
+ hash[:generator].each do |s|
186
+ if !s[:size_kw].nil? && (s[:size_kw] != 0)
187
+ @generator.push Generator.new(s)
188
+ if @total_generator_kw.nil?
189
+ @total_generator_kw = @generator[-1].size_kw
190
+ else
191
+ @total_generator_kw += @generator[-1].size_kw
192
+ end
193
+ end
194
+ end
195
+
196
+ @storage = []
197
+ if hash[:storage].class == Hash
198
+ hash[:storage] = [hash[:storage]]
199
+ elsif hash[:storage].nil?
200
+ hash[:storage] = []
201
+ end
202
+
203
+ hash[:storage].each do |s|
204
+ if !s[:size_kw].nil? && (s[:size_kw] != 0)
205
+ @storage.push Storage.new(s)
206
+ if @total_storage_kw.nil?
207
+ @total_storage_kw = @storage[-1].size_kw
208
+ @total_storage_kwh = @storage[-1].size_kwh
209
+ else
210
+ @total_storage_kw += @storage[-1].size_kw
211
+ @total_storage_kwh += @storage[-1].size_kwh
212
+ end
213
+ end
214
+ end
120
215
 
121
216
  # initialize class variables @@validator and @@schema
122
217
  @@validator ||= Validator.new
@@ -126,6 +221,49 @@ module URBANopt
126
221
  @@logger ||= URBANopt::Scenario::DefaultReports.logger
127
222
  end
128
223
 
224
+ ##
225
+ # Add a tech
226
+ ##
227
+ def add_tech(name, tech)
228
+ if name == 'solar_pv'
229
+ @solar_pv.push tech
230
+ if @total_solar_pv_kw.nil?
231
+ @total_solar_pv_kw = tech.size_kw
232
+ else
233
+ @total_solar_pv_kw += tech.size_kw
234
+ end
235
+ end
236
+
237
+ if name == 'wind'
238
+ @wind.push tech
239
+ if @total_wind_kw.nil?
240
+ @total_wind_kw = tech.size_kw
241
+ else
242
+ @total_wind_kw += tech.size_kw
243
+ end
244
+ end
245
+
246
+ if name == 'storage'
247
+ @storage.push tech
248
+ if @total_storage_kw.nil?
249
+ @total_storage_kw = tech.size_kw
250
+ @total_storage_kwh = tech.size_kwh
251
+ else
252
+ @total_storage_kw += tech.size_kw
253
+ @total_storage_kwh += tech.size_kwh
254
+ end
255
+ end
256
+
257
+ if name == 'generator'
258
+ @generator.push tech
259
+ if @total_generator_kw.nil?
260
+ @total_generator_kw = tech.size_kw
261
+ else
262
+ @total_generator_kw += tech.size_kw
263
+ end
264
+ end
265
+ end
266
+
129
267
  ##
130
268
  # Convert to a Hash equivalent for JSON serialization
131
269
  ##
@@ -137,12 +275,28 @@ module URBANopt
137
275
  result[:year_one_energy_cost_us_dollars] = @year_one_energy_cost_us_dollars if @year_one_energy_cost_us_dollars
138
276
  result[:year_one_demand_cost_us_dollars] = @year_one_demand_cost_us_dollars if @year_one_demand_cost_us_dollars
139
277
  result[:year_one_bill_us_dollars] = @year_one_bill_us_dollars if @year_one_bill_us_dollars
140
- result[:total_energy_cost_us_dollars] = @total_energy_cost_us_dollars if @total_energy_cost_us_dollars
141
- result[:solar_pv] = @solar_pv.to_hash if @solar_pv
142
- result[:wind] = @wind.to_hash if @wind
143
- result[:generator] = @generator.to_hash if @generator
144
- result[:storage] = @storage.to_hash if @storage
278
+ result[:total_solar_pv_kw] = @total_solar_pv_kw if @total_solar_pv_kw
279
+ result[:total_wind_kw] = @total_wind_kw if @total_wind_kw
280
+ result[:total_generator_kw] = @total_generator_kw if @total_generator_kw
281
+ result[:total_storage_kw] = @total_storage_kw if @total_storage_kw
282
+ result[:total_storage_kwh] = @total_storage_kwh if @total_storage_kwh
145
283
 
284
+ result[:solar_pv] = []
285
+ @solar_pv.each do |pv|
286
+ result[:solar_pv].push pv.to_hash
287
+ end
288
+ result[:wind] = []
289
+ @wind.each do |pv|
290
+ result[:wind].push wind.to_hash
291
+ end
292
+ result[:generator] = []
293
+ @generator.each do |pv|
294
+ result[:generator].push generator.to_hash
295
+ end
296
+ result[:storage] = []
297
+ @storage.each do |pv|
298
+ result[:storage].push storage.to_hash
299
+ end
146
300
  return result
147
301
  end
148
302
 
@@ -174,10 +328,43 @@ module URBANopt
174
328
  existing_dgen.year_one_bill_us_dollars = add_values(existing_dgen.year_one_bill_us_dollars, new_dgen.year_one_bill_us_dollars)
175
329
  existing_dgen.total_energy_cost_us_dollars = add_values(existing_dgen.total_energy_cost_us_dollars, new_dgen.total_energy_cost_us_dollars)
176
330
 
177
- existing_dgen.solar_pv = SolarPV.add_pv existing_dgen.solar_pv, new_dgen.solar_pv
178
- existing_dgen.wind = Wind.add_wind existing_dgen.wind, new_dgen.wind
179
- existing_dgen.generator = Generator.add_generator existing_dgen.generator, new_dgen.generator
180
- existing_dgen.storage = Storage.add_storage existing_dgen.storage, new_dgen.storage
331
+ new_dgen.solar_pv.each do |pv|
332
+ existing_dgen.solar_pv.push pv
333
+ if existing_dgen.total_solar_pv_kw.nil?
334
+ existing_dgen.total_solar_pv_kw = pv.size_kw
335
+ else
336
+ existing_dgen.total_solar_pv_kw += pv.size_kw
337
+ end
338
+ end
339
+
340
+ new_dgen.wind.each do |wind|
341
+ existing_dgen.wind.push wind
342
+ if existing_dgen.total_wind_kw.nil?
343
+ existing_dgen.total_wind_kw = wind.size_kw
344
+ else
345
+ existing_dgen.total_wind_kw += wind.size_kw
346
+ end
347
+ end
348
+
349
+ new_dgen.storage.each do |storage|
350
+ existing_dgen.storage.push storage
351
+ if existing_dgen.total_wind_kw.nil?
352
+ existing_dgen.total_storage_kw = storage.size_kw
353
+ existing_dgen.total_storage_kwh = storage.size_kwh
354
+ else
355
+ existing_dgen.total_storage_kw += storage.size_kw
356
+ existing_dgen.total_storage_kwh += storage.size_kwh
357
+ end
358
+ end
359
+
360
+ new_dgen.generator.each do |generator|
361
+ existing_dgen.generator.push generator
362
+ if existing_dgen.total_wind_kw.nil?
363
+ existing_dgen.total_generator_kw = generator.size_kw
364
+ else
365
+ existing_dgen.total_generator_kw += generator.size_kw
366
+ end
367
+ end
181
368
 
182
369
  return existing_dgen
183
370
  end
@@ -89,6 +89,9 @@ module URBANopt
89
89
  # initialize class variables @@validator and @@schema
90
90
  @@validator ||= Validator.new
91
91
  @@schema ||= @@validator.schema
92
+
93
+ # initialize feature report file name to be saved.
94
+ @file_name = 'default_feature_report'
92
95
  end
93
96
 
94
97
  ##
@@ -207,6 +210,50 @@ module URBANopt
207
210
 
208
211
  return result
209
212
  end
213
+
214
+ ##
215
+ # Saves the 'default_feature_report.json' and 'default_feature_report.csv' files
216
+ ##
217
+ # [parameters]:
218
+ # +file_name+ - _String_ - Assign a name to the saved feature report results file without an extension
219
+ def save_feature_report(file_name = 'updated_default_feature_report')
220
+ # reassign the initialize local variable @file_name to the file name input.
221
+ @file_name = file_name
222
+
223
+ # create feature reports directory
224
+ Dir.mkdir(File.join(@directory_name, 'feature_reports')) unless Dir.exist?(File.join(@directory_name, 'feature_reports'))
225
+
226
+ # save the csv data
227
+ old_timeseries_path = nil
228
+ if !@timeseries_csv.path.nil?
229
+ old_timeseries_path = @timeseries_csv.path
230
+ end
231
+
232
+ @timeseries_csv.path = File.join(@directory_name, 'feature_reports', file_name + '.csv')
233
+ @timeseries_csv.save_data
234
+
235
+ hash = {}
236
+ hash[:feature_report] = to_hash
237
+
238
+ json_name_path = File.join(@directory_name, 'feature_reports', file_name + '.json')
239
+
240
+ File.open(json_name_path, 'w') do |f|
241
+ f.puts JSON.pretty_generate(hash)
242
+ # make sure data is written to the disk one way or the other
243
+ begin
244
+ f.fsync
245
+ rescue StandardError
246
+ f.flush
247
+ end
248
+ end
249
+
250
+ if !old_timeseries_path.nil?
251
+ @timeseries_csv.path = old_timeseries_path
252
+ else
253
+ @timeseries_csv.path = File.join(@directory_name, 'feature_reports', file_name + '.csv')
254
+ end
255
+ return true
256
+ end
210
257
  end
211
258
  end
212
259
  end
@@ -44,7 +44,7 @@ module URBANopt
44
44
  :number_of_parking_spaces_charging, :parking_footprint_area, :maximum_parking_height, :maximum_number_of_parking_stories,
45
45
  :maximum_number_of_parking_stories_above_ground, :number_of_residential_units, :building_types, :building_type, :maximum_occupancy,
46
46
  :area, :window_area, :north_window_area, :south_window_area, :east_window_area, :west_window_area, :wall_area, :roof_area, :equipment_roof_area,
47
- :photovoltaic_roof_area, :available_roof_area, :total_roof_area, :orientation, :aspect_ratio # :nodoc:
47
+ :photovoltaic_roof_area, :available_roof_area, :total_roof_area, :orientation, :aspect_ratio, :total_construction_cost # :nodoc:
48
48
  # Program class initialize building program attributes: +:site_area+ , +:floor_area+ , +:conditioned_area+ , +:unconditioned_area+ ,
49
49
  # +:footprint_area+ , +:maximum_roof_height, +:maximum_number_of_stories+ , +:maximum_number_of_stories_above_ground+ , +:parking_area+ ,
50
50
  # +:number_of_parking_spaces+ , +:number_of_parking_spaces_charging+ , +:parking_footprint_area+ , +:maximum_parking_height+ , +:maximum_number_of_parking_stories+ ,
@@ -81,6 +81,7 @@ module URBANopt
81
81
  @roof_area = hash[:roof_area]
82
82
  @orientation = hash[:orientation]
83
83
  @aspect_ratio = hash[:aspect_ratio]
84
+ @total_construction_cost = hash[:total_construction_cost]
84
85
 
85
86
  # initialize class variables @@validator and @@schema
86
87
  @@validator ||= Validator.new
@@ -114,6 +115,7 @@ module URBANopt
114
115
  hash[:roof_area] = { equipment_roof_area: nil, photovoltaic_roof_area: nil, available_roof_area: nil, total_roof_area: nil }
115
116
  hash[:orientation] = nil
116
117
  hash[:aspect_ratio] = nil
118
+ hash[:total_construction_cost] = nil
117
119
  return hash
118
120
  end
119
121
 
@@ -167,6 +169,8 @@ module URBANopt
167
169
  result[:orientation] = @orientation if @orientation
168
170
  result[:aspect_ratio] = @aspect_ratio if @aspect_ratio
169
171
 
172
+ result[:total_construction_cost] = @total_construction_cost if @total_construction_cost
173
+
170
174
  # validate program properties against schema
171
175
  if @@validator.validate(@@schema[:definitions][:Program][:properties], result).any?
172
176
  raise "program properties does not match schema: #{@@validator.validate(@@schema[:definitions][:Program][:properties], result)}"
@@ -234,6 +238,7 @@ module URBANopt
234
238
  @maximum_number_of_parking_stories = max_value(@maximum_number_of_parking_stories, other.maximum_number_of_parking_stories)
235
239
  @maximum_number_of_parking_stories_above_ground = max_value(maximum_number_of_parking_stories_above_ground, other.maximum_number_of_parking_stories_above_ground)
236
240
  @number_of_residential_units = add_values(@number_of_residential_units, other.number_of_residential_units)
241
+ @total_construction_cost = add_values(@total_construction_cost, other.total_construction_cost)
237
242
 
238
243
  @building_types = other.building_types
239
244
 
@@ -46,7 +46,7 @@ module URBANopt
46
46
  :net_site_energy, :net_source_energy, :net_utility_cost, :electricity, :natural_gas, :additional_fuel, :district_cooling,
47
47
  :district_heating, :water, :electricity_produced, :end_uses, :energy_production, :photovoltaic, :utility_costs,
48
48
  :fuel_type, :total_cost, :usage_cost, :demand_cost, :comfort_result, :time_setpoint_not_met_during_occupied_cooling,
49
- :time_setpoint_not_met_during_occupied_heating, :time_setpoint_not_met_during_occupied_hours #:nodoc:
49
+ :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:
50
50
  # ReportingPeriod class initializes the reporting period attributes:
51
51
  # +:id+ , +:name+ , +:multiplier+ , +:start_date+ , +:end_date+ , +:month+ , +:day_of_month+ , +:year+ , +:total_site_energy+ , +:total_source_energy+ ,
52
52
  # +:net_site_energy+ , +:net_source_energy+ , +:net_utility_cost+ , +:electricity+ , +:natural_gas+ , +:additional_fuel+ , +:district_cooling+ ,
@@ -119,7 +119,8 @@ module URBANopt
119
119
  hash[:end_uses] = EndUses.new.to_hash
120
120
  hash[:energy_production] = { electricity_produced: { photovoltaic: nil } }
121
121
  hash[:utility_costs] = [{ fuel_type: nil, total_cost: nil, usage_cost: nil, demand_cost: nil }]
122
- hash[:comfort_result] = { time_setpoint_not_met_during_occupied_cooling: nil, time_setpoint_not_met_during_occupied_heating: nil, time_setpoint_not_met_during_occupied_hours: nil }
122
+ hash[:comfort_result] = { time_setpoint_not_met_during_occupied_cooling: nil, time_setpoint_not_met_during_occupied_heating: nil,
123
+ time_setpoint_not_met_during_occupied_hours: nil, hours_out_of_comfort_bounds_PMV: nil, hours_out_of_comfort_bounds_PPD: nil }
123
124
 
124
125
  return hash
125
126
  end
@@ -244,6 +245,8 @@ module URBANopt
244
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])
245
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])
246
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])
247
250
  end
248
251
 
249
252
  return existing_period
@@ -78,6 +78,7 @@ module URBANopt
78
78
  @timeseries_csv = TimeseriesCSV.new(hash[:timeseries_csv])
79
79
  @location = Location.new(hash[:location])
80
80
  @program = Program.new(hash[:program])
81
+ @distributed_generation = DistributedGeneration.new(hash[:distributed_generation] || {})
81
82
 
82
83
  @construction_costs = []
83
84
  hash[:construction_costs].each do |cc|
@@ -95,8 +96,8 @@ module URBANopt
95
96
  @feature_reports << FeatureReport.new(fr)
96
97
  end
97
98
 
98
- @distributed_generation = DistributedGeneration.new(hash[:distributed_generation] || {})
99
99
  @file_name = 'default_scenario_report'
100
+
100
101
  # initialize class variables @@validator and @@schema
101
102
  @@validator ||= Validator.new
102
103
  @@schema ||= @@validator.schema
@@ -142,10 +143,10 @@ module URBANopt
142
143
  end
143
144
 
144
145
  ##
145
- # Saves the 'default_feature_report.json' and 'default_scenario_report.csv' files
146
+ # Saves the 'default_scenario_report.json' and 'default_scenario_report.csv' files
146
147
  ##
147
148
  # [parameters]:
148
- # +file_name+ - _String_ - Assign a name to the saved scenario results file
149
+ # +file_name+ - _String_ - Assign a name to the saved scenario results file without an extension
149
150
  def save(file_name = 'default_scenario_report')
150
151
  # reassign the initialize local variable @file_name to the file name input.
151
152
  @file_name = file_name
@@ -181,7 +182,7 @@ module URBANopt
181
182
  if !old_timeseries_path.nil?
182
183
  @timeseries_csv.path = old_timeseries_path
183
184
  else
184
- @timeseries_csv.path = File.join(@directory_name, 'default_scenario_report.csv')
185
+ @timeseries_csv.path = File.join(@directory_name, file_name + '.csv')
185
186
  end
186
187
  return true
187
188
  end
@@ -243,14 +244,24 @@ module URBANopt
243
244
  # +feature_report+ - _FeatureReport_ - An object of FeatureReport class.
244
245
  ##
245
246
  def add_feature_report(feature_report)
246
- if @timesteps_per_hour.nil?
247
+ # check if the timesteps_per_hour are identical
248
+ if @timesteps_per_hour.nil? || @timesteps_per_hour == ''
247
249
  @timesteps_per_hour = feature_report.timesteps_per_hour
248
250
  else
249
- if feature_report.timesteps_per_hour != @timesteps_per_hour
251
+ if feature_report.timesteps_per_hour.is_a?(Integer) && feature_report.timesteps_per_hour != @timesteps_per_hour
250
252
  raise "FeatureReport timesteps_per_hour = '#{feature_report.timesteps_per_hour}' does not match scenario timesteps_per_hour '#{@timesteps_per_hour}'"
251
253
  end
252
254
  end
253
255
 
256
+ # check if first report_report_datetime are identical.
257
+ if @timeseries_csv.first_report_datetime.nil? || @timeseries_csv.first_report_datetime == ''
258
+ @timeseries_csv.first_report_datetime = feature_report.timeseries_csv.first_report_datetime
259
+ else
260
+ if feature_report.timeseries_csv.first_report_datetime != @timeseries_csv.first_report_datetime
261
+ raise "first_report_datetime '#{@first_report_datetime}' does not match other.first_report_datetime '#{feature_report.timeseries_csv.first_report_datetime}'"
262
+ end
263
+ end
264
+
254
265
  # check that we have not already added this feature
255
266
  id = feature_report.id
256
267
  @feature_reports.each do |existing_feature_report|