urbanopt-scenario 0.1.1 → 0.2.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
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|