ce-bucketize 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.
@@ -0,0 +1,251 @@
1
+ # Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
2
+
3
+ require 'ce-bucketize/model/hour_value'
4
+ require 'ce-greenbutton/model/gb_data_reading'
5
+ require 'interpolate'
6
+
7
+ module Bucketize
8
+ # An Enumerable for hourly consumption values. The values are aggregated from
9
+ # the raw GreenButton data.
10
+ #
11
+ # Author: ahmed.seddiq
12
+ # Version: 1.0
13
+ class HourlyValues
14
+ include Enumerable
15
+
16
+ # hour in seconds
17
+ HOUR = 3600
18
+
19
+ # The length (in seconds) of the maximum allowed gap in input data.
20
+ attr_accessor :max_gap_length
21
+
22
+ # This is a readonly attribute that will hold the combined interpolated
23
+ # raw data. It will be initialized in the constructor.
24
+ attr_reader :readings
25
+
26
+
27
+ # Creates a new instance of the HourlyValues collection.
28
+ #
29
+ # gb_data_description - the input GreenButton data.
30
+ # from - the start Time, must represent an hour start.
31
+ # to - the end Time, must represent an hour start.
32
+ #
33
+ # Raises ArgumentError if from or to don't represent an hour start.
34
+ def initialize(gb_data_description, from=nil, to=nil)
35
+ unless from.nil? or (from.min == 0 and from.sec == 0)
36
+ raise ArgumentError, '"from" should be an hour start'
37
+ end
38
+ unless to.nil? or (to.min == 0 and to.sec == 0)
39
+ raise ArgumentError, '"to" should be an hour start'
40
+ end
41
+
42
+ @gb_data_description = gb_data_description
43
+ @from = from
44
+ @to = to
45
+ @max_gap_length = 4 * HOUR
46
+ @readings = prepare_readings
47
+ end
48
+
49
+
50
+ # The "each" method required by the Enumerable mixin. It yields the hourly
51
+ # consumption values for the GreenButton data associated with this instance.
52
+ #
53
+ # Yields HourValue instances representing the hourly consumption of the data.
54
+ def each(&block)
55
+ return enum_for(:each) if block.nil?
56
+ readings = @readings.each
57
+
58
+ finished = false # will be set to true when raw data are all consumed.
59
+ cv = 0 # will aggregate current hour value
60
+ current_reading = readings.next
61
+ t0 = current_reading.time_start # the start of the first reading
62
+ d0 = current_reading.time_duration.to_f # duration of the first reading
63
+ # last reading time.
64
+ tn = @readings.last.time_start + @readings.last.time_duration - HOUR
65
+ hour_start = @from || t0 # will point to the current hour start
66
+ last_hour_start = ((@to - HOUR) unless @to.nil?) || tn # the last hour_start
67
+ d = (t0 + d0 - hour_start).to_f # remaining seconds from the current reading.
68
+ rem = (d/d0) * current_reading.value # remaining value from the current reading.
69
+ hf = 1.to_f # remaining fraction of hour for the current hour value
70
+ until finished do
71
+ # ratio to be consumed from the current reading
72
+ rat = [1.0, (hf * HOUR) / d].min
73
+ # increment current value by the ratio from the current reading
74
+ cv = cv + rat * rem
75
+ # decrement consumed value from current reading
76
+ rem = [0, rem * (1 - rat)].max
77
+ # update remaining fraction by the consumed duration
78
+ hf = [0, hf - ((rat * d)/ HOUR)].max
79
+ # update remaining duration
80
+ d = d * (1 - rat)
81
+
82
+ if hf == 0.0
83
+ # a complete hour is calculated
84
+ yield Bucketize::HourValue.new(hour_start, cv.round(2))
85
+ # reset the current hour value and fraction
86
+ cv = 0
87
+ hf = 1.to_f
88
+ hour_start += HOUR
89
+ if hour_start > last_hour_start
90
+ finished = true
91
+ end
92
+ end
93
+ if rem == 0
94
+ # current reading is totally consumed.
95
+ begin
96
+ # get next reading
97
+ current_reading = readings.next
98
+ d = current_reading.time_duration.to_f
99
+ rem = current_reading.value
100
+ rescue StopIteration
101
+ finished = true
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # Internal: this method will combine, sort, check for gaps and interpolates
108
+ # the raw data in the @gb_data_description.
109
+ #
110
+ # Returns the readings array; array of (GbDataReading)
111
+ #
112
+ # Raises TooManyDataMissingError if there is more than @max_gap_length
113
+ # missing data
114
+ #
115
+ private
116
+ def prepare_readings
117
+ # Combine all interval_readings in all gb_data instances.
118
+ readings = @gb_data_description.gb_data_array.reduce([]) do |all, gb_data|
119
+ all + gb_data.interval_readings
120
+ end
121
+ # Sort by reading time start
122
+ readings.sort_by! { |reading| reading.time_start }
123
+
124
+ # Compute average value per second
125
+ sec_avg = readings.reduce(0) do |avg, r|
126
+ avg + (r.value.to_f / r.time_duration.to_f)
127
+ end / readings.length
128
+
129
+ # check first reading
130
+ first_time = readings.first.time_start
131
+ if first_time.to_i % HOUR != 0
132
+ # add first reading to cover the missing part of the first hour.
133
+ first_reading = GbDataReading.new
134
+ first_reading.time_start = first_time - first_time.to_i % HOUR
135
+ first_reading.time_duration = first_time.to_i % HOUR
136
+ first_reading.value = (first_reading.time_duration * sec_avg).round(2)
137
+ readings = [first_reading] + readings
138
+ end
139
+ # check gaps
140
+ gaps = {}
141
+ readings.each_with_index do |reading, i|
142
+ next_time = reading.time_start + reading.time_duration
143
+ if reading.value.nil? or reading.value.to_f.nan? or reading.value == 0
144
+ # a gap detected
145
+ gaps[i] = {:from => reading.time_start, :to => next_time, :replace => true}
146
+ end
147
+
148
+ next_reading = readings[i + 1]
149
+ unless next_reading.nil? or (next_reading.time_start == next_time)
150
+ # a gap detected
151
+ unless next_reading.time_start - next_time <= @max_gap_length
152
+ raise Bucketize::TooManyDataMissingError
153
+ end
154
+ gaps[i + 1] = {:from => next_time, :to => next_reading.time_start}
155
+ end
156
+ end
157
+ # check last reading
158
+ last_time = readings.last.time_start + readings.last.time_duration
159
+ if last_time.to_i % HOUR != 0
160
+ # add last reading to cover the missing part of last hour.
161
+ last_reading = GbDataReading.new
162
+ last_reading.time_start = last_time
163
+ last_reading.time_duration = HOUR - last_time.to_i % HOUR
164
+ last_reading.value = (last_reading.time_duration * sec_avg).round(2)
165
+ readings << last_reading
166
+ end
167
+
168
+ # interpolation
169
+ interpolate_missing_data(gaps, readings)
170
+
171
+ # get start and end reading indicies
172
+ r0 = (get_reading_for(@from, readings) unless @from.nil?) || 0
173
+ rn = (get_reading_for(@to, readings) unless @to.nil?) ||
174
+ readings.length - 1
175
+
176
+ readings[r0..rn]
177
+ end
178
+
179
+ # Get the index of reading that covers the given time.
180
+ #
181
+ # time - the time to search for.
182
+ # reading - the reading array.
183
+ #
184
+ # Returns the index of the reading that covers this time
185
+
186
+ # Raises ArgumentError if not found.
187
+ def get_reading_for(time, readings)
188
+ readings.each_with_index do |reading, i|
189
+ if time.between? reading.time_start,
190
+ reading.time_start + reading.time_duration - 1
191
+ return i
192
+ end
193
+ end
194
+ raise ArgumentError, "#{time} is out of range"
195
+ end
196
+
197
+ # Internal: interpolates the given gaps in the readings.
198
+ #
199
+ # gaps - a hash {index_of_gap_in_readings => {:from=> time, :to =>time}}
200
+ # readings - the readings array
201
+ #
202
+ # Returns Nothing, the interpolated data will be injected to the readings
203
+ # array.
204
+ def interpolate_missing_data(gaps, readings)
205
+ if gaps.size > 0
206
+ # get points for interpolation
207
+ points = readings.reduce({}) do |all, reading|
208
+ unless reading.value.nil? or reading.value.to_f.nan? or
209
+ reading.value == 0
210
+ all[reading.time_start.to_i] = reading.value
211
+ end
212
+ all
213
+ end
214
+
215
+ interpolate = Interpolate::Points.new(points)
216
+ gaps_filled = 0
217
+ gaps.each_pair do |i, gap|
218
+ missing_index = i + gaps_filled # offset for previously filled gaps
219
+ if gap[:replace]
220
+ readings[missing_index].value = interpolate.at(readings[missing_index].time_start.to_i)
221
+ else
222
+ reading_before = readings[i - 1]
223
+ unless reading_before.nil?
224
+ missing_time = gap[:from]
225
+ until missing_time >= gap[:to]
226
+ new_reading = GbDataReading.new
227
+ new_reading.time_start = missing_time
228
+ new_reading.time_duration = reading_before.time_duration
229
+ new_reading.value = interpolate.at(missing_time.to_i)
230
+ readings.insert(missing_index, new_reading)
231
+ gaps_filled += 1
232
+ missing_index += 1
233
+ missing_time += reading_before.time_duration
234
+ end
235
+ end
236
+ end
237
+
238
+ end
239
+ end
240
+ end
241
+
242
+
243
+ end
244
+
245
+ # This error will be raised if large data gaps encountered while processing.
246
+ # by default, it will check for "4 hour" gaps; this can be configured by
247
+ # the "max_gap_length" attribute in seconds.
248
+ class TooManyDataMissingError < Exception
249
+
250
+ end
251
+ end
@@ -0,0 +1,27 @@
1
+ # Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
2
+
3
+ require 'ce-bucketize/model/hour_value'
4
+
5
+ module Bucketize
6
+ # The consumption cost for a given hour.
7
+ #
8
+ # Author: ahmed.seddiq
9
+ # Version: 1.0
10
+ class HourCost
11
+ # A Time object represents the start of the hour in the local time of the
12
+ # consumer.
13
+ attr_accessor :hour_start
14
+
15
+ # The consumption value for this hour.
16
+ attr_accessor :value
17
+
18
+ # The consumption cost.
19
+ attr_accessor :cost
20
+
21
+ def initialize(hour_start, value, cost)
22
+ @hour_start = hour_start
23
+ @value = value
24
+ @cost = cost
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
2
+
3
+ module Bucketize
4
+ # The consumption value for a given hour.
5
+ #
6
+ # Author: ahmed.seddiq
7
+ # Version: 1.0
8
+ class HourValue
9
+ # A Time object represents the start of the hour in the local time of the
10
+ # consumer.
11
+ attr_accessor :hour_start
12
+
13
+ # The consumption value for this hour.
14
+ attr_accessor :value
15
+
16
+ def initialize(hour_start, value)
17
+ @hour_start = hour_start
18
+ @value = value
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,134 @@
1
+ # Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
2
+
3
+ require 'json'
4
+ require 'ce-bucketize'
5
+
6
+ module Bucketize
7
+ # A TariffRule is the rule that defines the way in which data will be
8
+ # aggregated.
9
+ #
10
+ # It provides the "applies_to" method which checks a given point in time
11
+ # against this rule.
12
+ #
13
+ # Author: ahmed.seddiq
14
+ # Version: 1.0
15
+ class TariffRule
16
+ # The maximum integer for the current machine.
17
+ MAX_INT = (2**((0.size * 8) - 1)) - 1
18
+
19
+ # The utility ID related to this TariffRule.
20
+ attr_accessor :utility_id
21
+
22
+ # The rate class related to this TariffRule.
23
+ attr_accessor :rate_class
24
+
25
+ # The start Date for which this TariffRule is applied. Can be nil.
26
+ attr_accessor :start_date
27
+
28
+ # The end Date for which this TariffRule is applied. Can be nil.
29
+ attr_accessor :end_date
30
+
31
+ # The time start (hour in day) for which this TariffRule is applied.
32
+ # Defaults to 1
33
+ attr_accessor :time_start
34
+
35
+ # The time end (hour in day) for which this TariffRule is applied.
36
+ # Defaults to 24
37
+ attr_accessor :time_end
38
+
39
+ # An array of days for which this TariffRule is applied. Monday is 1 and
40
+ # Sunday is 7
41
+ attr_accessor :day_of_week
42
+
43
+ # The lower bound of the monthly aggregated consumption value for which this
44
+ # TariffRule is applied. Can be nil.
45
+ attr_accessor :kwh_low
46
+
47
+ # The upper bound of the monthly aggregated consumption value for which this
48
+ # TariffRule is applied. Can be nil.
49
+ attr_accessor :kwh_high
50
+
51
+ # The tariff value.
52
+ attr_accessor :tariff
53
+
54
+ # Sets the default value for attributes.
55
+ def initialize
56
+ @time_start = 1
57
+ @time_end = 24
58
+ end
59
+
60
+ # Validates the fields of this rule.
61
+ #
62
+ # Raises Bucketize::InvalidTariffError if invalid.
63
+ def validate
64
+ not_valid_err = Bucketize::InvalidTariffError
65
+ raise not_valid_err, 'tariff is required' if @tariff.nil?
66
+ raise not_valid_err, 'time_start is require' if @time_start.nil?
67
+ raise not_valid_err, 'time_end is required' if @time_end.nil?
68
+ raise not_valid_err, 'time_end < time_start' if @time_end < @time_start
69
+ raise not_valid_err, 'time not in 1..24' unless @time_end.between? 1, 24
70
+ raise not_valid_err, 'time not in 1..24' unless @time_start.between? 1, 24
71
+ raise not_valid_err, 'utility_id is required' if @utility_id.nil?
72
+ raise not_valid_err, 'rate_class is required' if @rate_class.nil?
73
+
74
+ if not @end_date.nil? and not @start_date.nil?
75
+ raise not_valid_err, 'end_date < start_date' if @end_date < @start_date
76
+ end
77
+ unless @day_of_week.nil? or @day_of_week.all? { |d| d.between? 1, 7 }
78
+ raise not_valid_err, 'day_of_week invalid, not in 1..7 range'
79
+ end
80
+ end
81
+
82
+ # Public: Checks if this rule applies to the given time and aggregated
83
+ # value.
84
+ #
85
+ # time - the Time to check against this rule.
86
+ # total_kwh - the aggregated value of the consumption till the given time.
87
+ #
88
+ # Returns true if this rule applies to the given time and aggregated value.
89
+ def applies_to?(time, total_kwh=nil)
90
+ start_date = @start_date || Time.at(0).utc
91
+ end_date = @end_date || Time.at(MAX_INT)
92
+ day_of_week = @day_of_week || (1..7).to_a
93
+ kwh_low = @kwh_low || 0
94
+ kwh_high = @kwh_high || MAX_INT
95
+
96
+ wday = time.wday
97
+ wday = 7 if wday == 0 # sunday is 7 not 0
98
+ hour = time.hour + 1 # hour from 1 - 24
99
+ time.between? start_date, end_date and
100
+ hour.between? @time_start, @time_end and
101
+ day_of_week.include? wday and
102
+ (total_kwh.nil? or total_kwh.between? kwh_low, kwh_high)
103
+ end
104
+
105
+
106
+ # This operator will be called by JSON.parse to decode the json to an
107
+ # instance of TariffRule
108
+ #
109
+ # key - the property name
110
+ # value - the property value
111
+ def []=(key, value)
112
+ # convert date properties
113
+ if %w(start_date end_date).include? key and !value.nil?
114
+ value = Date.parse(value)
115
+ end
116
+ # call the setter
117
+ public_send(key + '=', value)
118
+ end
119
+
120
+ # The equal operator. Two rules are equal if and only if all instance
121
+ # variables are equal.
122
+ #
123
+ # Returns whether this object equals the given object.
124
+ def ==(other)
125
+ equals = true
126
+ instance_variables.each do |property|
127
+ equals &&= (instance_variable_get(property) ==
128
+ other.instance_variable_get(property))
129
+ end
130
+ equals
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,50 @@
1
+ # Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
2
+
3
+ require 'ce-bucketize/model/hour_cost'
4
+
5
+ module Bucketize
6
+ # A collection of tariffed hourly consumption costs. This collection will be
7
+ # initialized by the input HourlyValues and the TariffRule(s). This collection
8
+ # will provide the costs after applying the given TariffRule(s).
9
+ #
10
+ # Author: ahmed.seddiq
11
+ # Version: 1.0
12
+ class TariffedHourlyCosts
13
+ include Enumerable
14
+
15
+ # Initializes this instance with the given hourly values and tariff rules.
16
+ #
17
+ # hourly_values - the Collection of hourly values on which the tariff rules
18
+ # will be applied.
19
+ # tariff_rules - the Array of TariffRules to be used for aggregation.
20
+ #
21
+ def initialize(hourly_values, tariff_rules)
22
+ @hourly_values = hourly_values
23
+ @tariff_rules = tariff_rules
24
+ end
25
+
26
+ # The "each" method required by the Enumerable mixin. It yields the HourCost
27
+ # instances after applying the given TariffRule(s).
28
+ #
29
+ # Yields HourCost instances representing the tariffed hourly cost of the data.
30
+ def each(&block)
31
+ return enum_for(:each) if block.nil?
32
+
33
+ total_kwh = 0
34
+ @hourly_values.each do |h_value|
35
+ total_kwh += h_value.value
36
+ matched_rule = @tariff_rules.find {|rule| rule.applies_to? h_value.hour_start, total_kwh}
37
+ tariff = 1
38
+ tariff = matched_rule.tariff unless matched_rule.nil?
39
+ yield Bucketize::HourCost.new(h_value.hour_start, h_value.value,
40
+ h_value.value * tariff)
41
+ end
42
+ end
43
+ end
44
+
45
+ # This error will be raised if no matched tariff found while calculating the
46
+ # costs.
47
+ class NoMatchedTariffError < Exception
48
+
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ # Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
2
+
3
+ module Bucketize
4
+ # An Enumerable for tariffed hourly consumption values. This collection will be
5
+ # initialized by the input HourlyValues and the TariffRule(s). It will provide
6
+ # the values after applying the given TariffRule(s). If no rule was matched
7
+ # , the corresponding hourly value will be returned.
8
+ #
9
+ #
10
+ # Author: ahmed.seddiq
11
+ # Version: 1.0
12
+ class TariffedHourlyValues
13
+ include Enumerable
14
+
15
+ # Initializes this instance with the given hourly values and tariff rules.
16
+ #
17
+ # hourly_values - the Collection of hourly values on which the tariff rules
18
+ # will be applied.
19
+ # tariff_rules - the Array of TariffRules to be used for aggregation.
20
+ #
21
+ def initialize(hourly_values, tariff_rules)
22
+ @hourly_values = hourly_values
23
+ @tariff_rules = tariff_rules
24
+ end
25
+
26
+ # The "each" method required by the Enumerable mixin. It yields the HourCost
27
+ # instances after applying the given TariffRule(s).
28
+ #
29
+ # Yields HourCost instances representing the tariffed hourly cost of the data.
30
+ def each(&block)
31
+ return enum_for(:each) if block.nil?
32
+
33
+ total_kwh = 0
34
+ @hourly_values.each do |h_value|
35
+ total_kwh += h_value.value
36
+ @tariff_rules.each do |rule|
37
+ if rule.applies_to? h_value.hour_start, total_kwh
38
+ h_value.value = h_value.value * rule.tariff
39
+ break
40
+ end
41
+ end
42
+ yield h_value
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,5 @@
1
+ # Copyright (C) 2015 TopCoder Inc., All Rights Reserved.
2
+
3
+ module Bucketize
4
+ VERSION = "0.1.0"
5
+ end