technical_graph 0.3.0 → 0.3.1

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.
@@ -1,212 +1,42 @@
1
1
  #encoding: utf-8
2
2
 
3
- # Approximate data layer
3
+ require 'technical_graph/data_layer_processor_simple_smoother'
4
+ require 'technical_graph/data_layer_processor_noise_removal'
4
5
 
5
- class DataLayerProcessor
6
- STRATEGIES = {
7
- :rectangular => 'generate_vector_rectangular',
8
- :gauss => 'generate_vector_gauss'
9
- }
10
- DEFAULT_STRATEGY = :rectangular
11
-
12
- MIN_LEVEL = 1
13
- MAX_LEVEL = 200
14
-
15
- # use 'x' axis for processing also
16
- PROCESS_WITH_PARAMETER_DISTANCE = false
6
+ # Smooth data layer, approximate, ..
17
7
 
18
- # default Gauss coefficient
19
- DEFAULT_GAUSS_COEFF = 0.2
8
+ class DataLayerProcessor
9
+ include DataLayerProcessorSimpleSmoother
10
+ include DataLayerProcessorNoiseRemoval
20
11
 
21
12
  def initialize(data_layer)
22
13
  @data_layer = data_layer
23
- @strategy = DEFAULT_STRATEGY
24
- @level = MIN_LEVEL
25
- @vector = Array.new
26
- @gauss_coeff = DEFAULT_GAUSS_COEFF
14
+ simple_smoother_initialize(data_params)
15
+ noise_removal_initialize(data_params)
27
16
  end
28
17
 
29
- attr_reader :vector
30
- attr_accessor :gauss_coeff
31
-
32
- # Level of approximation
33
- def level=(l)
34
- @level = l.to_i if l.to_i >= MIN_LEVEL and l.to_i < MAX_LEVEL
18
+ # Additional layer parameters, processors options
19
+ def data_params
20
+ @data_layer.data_params
35
21
  end
36
22
 
37
- attr_reader :level
38
-
39
- # Choose other strategy from STRATEGIES
40
- def strategy=(s)
41
- method = STRATEGIES[s]
42
- @strategy = s unless method.nil?
23
+ # Data from DataLayer, not raw data
24
+ def data
25
+ @data_layer.processed_data
43
26
  end
44
27
 
45
- attr_reader :strategy
46
-
47
-
48
- # This vector will be used to process values (Y'es)
49
- # Use proper strategy
50
- def generate_vector
51
- method = STRATEGIES[@strategy]
52
- if method.nil?
53
- method = STRATEGIES[DEFAULT_STRATEGY]
54
- end
55
- return self.send(method)
56
- end
57
-
58
- # Process values
59
28
  def process
60
- t = Time.now
61
- old_data = @data_layer.data
62
- new_data = Array.new
63
-
64
- (0...old_data.size).each do |i|
65
- new_data << {
66
- :x => old_data[i][:x],
67
- :y => process_part(old_data, i)
68
- }
69
- end
70
-
71
- puts "Smoothing completed, level #{level}, data size #{old_data.size}, time #{Time.now - t}"
72
-
73
- return new_data
74
- end
75
-
76
- # Process part (size depends on level)
77
- def process_part(old_data, position)
78
- # neutral data, used where position is near edge to calculate new value
79
- neutral_data = {
80
- :x => old_data[position][:x],
81
- :y => old_data[position][:y]
82
- }
83
- part_array = Array.new(level, neutral_data)
84
-
85
- # add data from old_data to part_array
86
- offset = (level/2.0).floor
87
- # copying data
88
- (0...level).each do |l|
89
- copy_pos = position + l - offset
90
- # if copy_pos is inside data
91
- if copy_pos >= 0 and old_data.size > copy_pos
92
- part_array[l] = old_data[copy_pos]
93
- end
94
- end
95
- # here we should have part_array and vector
96
- # and now do some magic :]
97
-
98
- if PROCESS_WITH_PARAMETER_DISTANCE == false
99
- y_sum = 0.0
100
- (0...level).each do |l|
101
- y_sum += part_array[l][:y] * vector[l]
102
- end
103
- return y_sum
104
- else
105
- # TODO bugs!, issues with NaN
106
- # waged average using inverted distance
107
- _sum = 0.0
108
- _wages = 0.0
109
- _x_position = old_data[position][:x]
110
-
111
- (0...level).each do |l|
112
- _x_distance = (part_array[l][:x] - _x_position).abs
113
- _wage = (1.0 / _x_distance)
114
-
115
- unless _wage.nan?
116
- _wages += _wage
117
- _sum += (part_array[l][:y] * vector[l]) / _x_distance
118
- end
119
- end
120
- y = _sum.to_f / _wages.to_f
121
- puts y
122
- return y
123
- end
124
-
125
- end
29
+ # before processing old processed data is overwritten by cloned raw data
30
+ @data = data
126
31
 
32
+ # update params before processing
33
+ simple_smoother_initialize(data_params)
34
+ noise_removal_initialize(data_params)
127
35
 
128
- # This vector will be used to process values (Y'es), linear algorithm
129
- def generate_vector_rectangular
130
- @vector = Array.new
131
- # calculated
132
- (1..level).each do |i|
133
- @vector << 1.0 / level.to_f
134
- end
135
- return @vector
36
+ # TODO add in options array to choose order of these methods
37
+ noise_removal_process
38
+ simple_smoother_process
39
+
40
+ return @data
136
41
  end
137
-
138
- # This vector will be used to process values (Y'es), linear algorithm
139
- def generate_vector_gauss
140
- # http://www.techotopia.com/index.php/Ruby_Math_Functions_and_Methods#Ruby_Math_Constants
141
- # http://pl.wikipedia.org/wiki/Okno_czasowe
142
-
143
- # calculation
144
- count = (level.to_f / 2.0).floor + 1
145
-
146
- v = Array.new
147
- # calculated
148
- (1..count).each do |i|
149
- v << Math::E ** ((-0.5) * (i*gauss_coeff) ** 2)
150
- end
151
-
152
- @vector = make_mirror(v, level)
153
-
154
- normalize_vector
155
-
156
- return @vector
157
- end
158
-
159
- # Multiply vector to have sum eq. 1.0
160
- def normalize_vector
161
- s = 0.0
162
- @vector.each do |v|
163
- s += v
164
- end
165
-
166
- new_vector = Array.new
167
-
168
- @vector.each do |v|
169
- new_vector << v / s
170
- end
171
-
172
- @vector = new_vector
173
-
174
- return @vector
175
- end
176
-
177
- # Make mirror array
178
- # size = 7 => [ i[3], i[2], i[1], i[0], i[1], i[2], i[3] ]
179
- # size = 8 => [ i[3], i[2], i[1], i[0], i[0], i[1], i[2], i[3] ]
180
- def make_mirror(input, size)
181
- a = Array.new(size, 0.1)
182
- if size.even?
183
- # two 'first' in central
184
- c_left = size/2 - 1
185
- c_right = size/2
186
-
187
- a[c_left] = input[0]
188
- a[c_right] = input[0]
189
- else
190
- # there is one 'first'
191
- c_left = (size/2.0).floor
192
- c_right = (size/2.0).floor
193
-
194
- a[c_left] = input[0]
195
- # a[c_right] = input[0]
196
- end
197
-
198
- # the rest
199
- i = 0
200
- while c_left > 0
201
- i += 1
202
- c_left -= 1
203
- c_right += 1
204
-
205
- a[c_left] = input[i]
206
- a[c_right] = input[i]
207
- end
208
-
209
- return a
210
- end
211
-
212
42
  end
@@ -0,0 +1,90 @@
1
+ #encoding: utf-8
2
+
3
+ require 'technical_graph/array'
4
+
5
+ module DataLayerProcessorNoiseRemoval
6
+ DEFAULT_NOISE_REMOVAL_LEVEL = 3
7
+ DEFAULT_NOISE_REMOVAL_WINDOW_SIZE = 10
8
+
9
+ def noise_removal_initialize(options)
10
+ @noise_removal = options[:noise_removal] == true
11
+ @noise_removal_level = options[:noise_removal_level] || DEFAULT_NOISE_REMOVAL_LEVEL
12
+ @noise_removal_window_size = options[:noise_removal_window_size] || DEFAULT_NOISE_REMOVAL_WINDOW_SIZE
13
+ end
14
+
15
+ attr_accessor :noise_removal_level, :noise_removal_window_size, :noise_removal
16
+
17
+ # Smooth values
18
+ def noise_removal_process
19
+ return if noise_removal == false
20
+
21
+ t = Time.now
22
+ new_data = Array.new
23
+
24
+ @noises_removed_count = 0
25
+
26
+ puts "Noise removal started"
27
+
28
+ (0...data.size).each do |i|
29
+ if not noise?(i)
30
+ new_data << data[i]
31
+ else
32
+ @noises_removed_count += 1
33
+ end
34
+ end
35
+
36
+ puts "Noise removal completed, removed #{@noises_removed_count}, time #{Time.now - t}"
37
+
38
+ @data = new_data
39
+ return new_data
40
+ end
41
+
42
+ def noise_removal_window_from(i)
43
+ return i - (noise_removal_window_size.to_f / 2.0).ceil
44
+ end
45
+
46
+ def noise_removal_window_to(i)
47
+ return i + (noise_removal_window_size.to_f / 2.0).ceil
48
+ end
49
+
50
+ # Check if data at index is noisy
51
+ def noise?(i)
52
+ i_from = noise_removal_window_from(i)
53
+ i_to = noise_removal_window_to(i)
54
+
55
+ # create partial array, TODO move it somewhere else
56
+ part_array = Array.new
57
+ border = false
58
+
59
+ (i_from..i_to).each do |current_i|
60
+ # outside ranges
61
+ if current_i < 0
62
+ part_array << data.first
63
+ border = true
64
+ next
65
+ end
66
+
67
+ if data.size <= current_i
68
+ part_array << data.last
69
+ border = true
70
+ next
71
+ end
72
+
73
+ part_array << data[current_i]
74
+
75
+ end
76
+ y_mean = part_array.collect { |p| p.y }.mean
77
+
78
+ # another algorithm
79
+ noise_strength = (data[i].y - y_mean).abs / y_mean
80
+ return noise_strength_enough?(noise_strength)
81
+ end
82
+
83
+ # Some magic here, beware
84
+ def noise_strength_enough?(noise_strength)
85
+ threshold_strength = Math.log(@noise_removal_level)
86
+ return noise_strength > threshold_strength
87
+ end
88
+
89
+
90
+ end
@@ -0,0 +1,234 @@
1
+ #encoding: utf-8
2
+
3
+ module DataLayerProcessorSimpleSmoother
4
+ # Smoothing
5
+ SIMPLE_SMOOTHER_STRATEGIES = {
6
+ :rectangular => 'generate_vector_rectangular',
7
+ :gauss => 'generate_vector_gauss'
8
+ }
9
+ DEFAULT_SIMPLE_SMOOTHER_STRATEGY = :rectangular
10
+
11
+ MIN_SIMPLE_SMOOTHER_LEVEL = 1
12
+ MAX_SIMPLE_SMOOTHER_LEVEL = 200
13
+
14
+ # use 'x' axis for processing also
15
+ PROCESS_WITH_PARAMETER_DISTANCE = false
16
+
17
+ # default Gauss coefficient
18
+ DEFAULT_GAUSS_COEFF = 0.2
19
+
20
+ def simple_smoother_initialize(options)
21
+ @simple_smoother = options[:simple_smoother] == true
22
+ @simple_smoother_strategy = options[:simple_smoother_strategy] || DEFAULT_SIMPLE_SMOOTHER_STRATEGY
23
+ @simple_smoother_level = options[:simple_smoother_level] || MIN_SIMPLE_SMOOTHER_LEVEL
24
+ @simple_smoother_x = options[:simple_smoother_x] == true
25
+
26
+ @vector = Array.new
27
+ @gauss_coeff = DEFAULT_GAUSS_COEFF
28
+ end
29
+
30
+ attr_reader :vector, :simple_smoother
31
+ attr_accessor :gauss_coeff
32
+
33
+ # Simple_smoother_level of approximation
34
+ def simple_smoother_level=(l)
35
+ @simple_smoother_level = l.to_i if l.to_i >= MIN_SIMPLE_SMOOTHER_LEVEL and l.to_i < MAX_SIMPLE_SMOOTHER_LEVEL
36
+ end
37
+
38
+ attr_reader :simple_smoother_level
39
+
40
+ # Choose other simple_smoother_strategy from SIMPLE_SMOOTHER_STRATEGIES
41
+ def simple_smoother_strategy=(s)
42
+ method = SIMPLE_SMOOTHER_STRATEGIES[s]
43
+ @simple_smoother_strategy = s unless method.nil?
44
+ end
45
+
46
+ attr_reader :simple_smoother_strategy
47
+
48
+ # smooth using X distance
49
+ attr_accessor :simple_smoother_x
50
+
51
+ # This vector will be used to process values (Y'es)
52
+ # Use proper simple_smoother_strategy
53
+ def generate_vector
54
+ method = SIMPLE_SMOOTHER_STRATEGIES[@simple_smoother_strategy]
55
+ if method.nil?
56
+ method = SIMPLE_SMOOTHER_STRATEGIES[DEFAULT_SIMPLE_SMOOTHER_STRATEGY]
57
+ end
58
+ return self.send(method)
59
+ end
60
+
61
+ # Smooth values
62
+ def simple_smoother_process
63
+ # not turned on
64
+ return if simple_smoother == false
65
+
66
+ # vector used for smoothing
67
+ generate_vector
68
+
69
+ t = Time.now
70
+ old_data = @data
71
+ new_data = Array.new
72
+
73
+ # pre-processing, distance
74
+ if simple_smoother_x == true
75
+ puts "X axis distance smoothing enabled"
76
+
77
+ (0...old_data.size).each do |i|
78
+ new_data << DataPoint.xy(old_data[i].x, process_part(old_data, i, false))
79
+ end
80
+
81
+ old_data = new_data
82
+ new_data = Array.new
83
+ end
84
+
85
+ puts "Y axis distance smoothing"
86
+
87
+ (0...old_data.size).each do |i|
88
+ new_data << DataPoint.xy(old_data[i].x, process_part(old_data, i))
89
+ end
90
+
91
+ puts "Smoothing completed, simple_smoother_level #{simple_smoother_level}, data size #{old_data.size}, time #{Time.now - t}"
92
+
93
+ @data = new_data
94
+ return new_data
95
+ end
96
+
97
+ # Process part (size depends on simple_smoother_level)
98
+ def process_part(old_data, position, y_based = true)
99
+ # neutral data, used where position is near edge to calculate new value
100
+ neutral_data = DataPoint.xy(old_data[position].x, old_data[position].y)
101
+ part_array = Array.new(simple_smoother_level, neutral_data)
102
+
103
+ # add data from old_data to part_array
104
+ offset = (simple_smoother_level/2.0).floor
105
+ # copying data
106
+ (0...simple_smoother_level).each do |l|
107
+ copy_pos = position + l - offset
108
+ # if copy_pos is inside data
109
+ if copy_pos >= 0 and old_data.size > copy_pos
110
+ part_array[l] = old_data[copy_pos]
111
+ end
112
+ end
113
+ # here we should have part_array and vector
114
+ # and now do some magic :]
115
+
116
+
117
+ if y_based
118
+ return process_part_only_y(part_array)
119
+ else
120
+ return process_part_only_x(part_array, neutral_data)
121
+ end
122
+ end
123
+
124
+ # Process part (size depends on simple_smoother_level), only Y data
125
+ def process_part_only_y(part_array, neutral_data = nil)
126
+ y_sum = 0.0
127
+ (0...simple_smoother_level).each do |l|
128
+ y_sum += part_array[l].y * vector[l]
129
+ end
130
+ return y_sum
131
+ end
132
+
133
+ # Process part (size depends on simple_smoother_level), Y and X data
134
+ def process_part_only_x(part_array, neutral_data)
135
+ weights = Array.new
136
+ w_sum = 0.0
137
+ (0...simple_smoother_level).each do |l|
138
+ p = part_array[l]
139
+ x_distance = p.x_distance(neutral_data)
140
+ w = (Math::E ** (-1.0 * 0.2 * x_distance)) + 1.0
141
+ w_sum += w
142
+ weights << w
143
+ end
144
+
145
+ w_prod = 0.0
146
+ part_array.each_index { |i| w_prod += part_array[i].y * weights[i].to_f }
147
+ return w_prod.to_f / w_sum.to_f
148
+ end
149
+
150
+ # This vector will be used to process values (Y'es), linear algorithm
151
+ def generate_vector_rectangular
152
+ @vector = Array.new
153
+ # calculated
154
+ (1..simple_smoother_level).each do |i|
155
+ @vector << 1.0 / simple_smoother_level.to_f
156
+ end
157
+ return @vector
158
+ end
159
+
160
+ # This vector will be used to process values (Y'es), linear algorithm
161
+ def generate_vector_gauss
162
+ # http://www.techotopia.com/index.php/Ruby_Math_Functions_and_Methods#Ruby_Math_Constants
163
+ # http://pl.wikipedia.org/wiki/Okno_czasowe
164
+
165
+ # calculation
166
+ count = (simple_smoother_level.to_f / 2.0).floor + 1
167
+
168
+ v = Array.new
169
+ # calculated
170
+ (1..count).each do |i|
171
+ v << Math::E ** ((-0.5) * (i*gauss_coeff) ** 2)
172
+ end
173
+
174
+ @vector = make_mirror(v, simple_smoother_level)
175
+
176
+ normalize_vector
177
+
178
+ return @vector
179
+ end
180
+
181
+ # Multiply vector to have sum eq. 1.0
182
+ def normalize_vector
183
+ s = 0.0
184
+ @vector.each do |v|
185
+ s += v
186
+ end
187
+
188
+ new_vector = Array.new
189
+
190
+ @vector.each do |v|
191
+ new_vector << v / s
192
+ end
193
+
194
+ @vector = new_vector
195
+
196
+ return @vector
197
+ end
198
+
199
+ # Make mirror array
200
+ # size = 7 => [ i[3], i[2], i[1], i[0], i[1], i[2], i[3] ]
201
+ # size = 8 => [ i[3], i[2], i[1], i[0], i[0], i[1], i[2], i[3] ]
202
+ def make_mirror(input, size)
203
+ a = Array.new(size, 0.1)
204
+ if size.even?
205
+ # two 'first' in central
206
+ c_left = size/2 - 1
207
+ c_right = size/2
208
+
209
+ a[c_left] = input[0]
210
+ a[c_right] = input[0]
211
+ else
212
+ # there is one 'first'
213
+ c_left = (size/2.0).floor
214
+ c_right = (size/2.0).floor
215
+
216
+ a[c_left] = input[0]
217
+ # a[c_right] = input[0]
218
+ end
219
+
220
+ # the rest
221
+ i = 0
222
+ while c_left > 0
223
+ i += 1
224
+ c_left -= 1
225
+ c_right += 1
226
+
227
+ a[c_left] = input[i]
228
+ a[c_right] = input[i]
229
+ end
230
+
231
+ return a
232
+ end
233
+
234
+ end
@@ -0,0 +1,43 @@
1
+ class DataPoint
2
+
3
+ def initialize(h={ :x => 0, :y => 0 })
4
+ if h.kind_of? Hash
5
+ @x = h[:x]
6
+ @y = h[:y]
7
+ end
8
+ if h.kind_of? DataPoint
9
+ @x = h.x
10
+ @y = h.y
11
+ end
12
+ end
13
+
14
+ def self.xy(_x, _y)
15
+ DataPoint.new({ :x=>_x, :y=>_y })
16
+ end
17
+
18
+ def x_distance(other_dp)
19
+ return (self.x - other_dp.x).abs
20
+ end
21
+
22
+ def y_distance(other_dp)
23
+ return (self.y - other_dp.y).abs
24
+ end
25
+
26
+ def x
27
+ @x
28
+ end
29
+
30
+ def y
31
+ @y
32
+ end
33
+
34
+ def x=(_x)
35
+ @x = _x
36
+ end
37
+
38
+ def y=(_y)
39
+ @y = _y
40
+ end
41
+
42
+
43
+ end
@@ -42,6 +42,14 @@ class GraphAxis
42
42
  options[:y_axis_fixed_interval] == true
43
43
  end
44
44
 
45
+ def y_axis_interval
46
+ options[:y_axis_interval]
47
+ end
48
+
49
+ def x_axis_interval
50
+ options[:x_axis_interval]
51
+ end
52
+
45
53
  # Where to put axis values
46
54
  def value_axis
47
55
  return calc_axis(data_processor.y_min, data_processor.y_max, options[:y_axis_interval], options[:y_axis_count], y_axis_fixed?)
@@ -178,9 +186,10 @@ class GraphAxis
178
186
 
179
187
  t = Time.now
180
188
  plot_axis_y_line.draw(@image)
181
- puts "#{Time.now - t} drawing lines"
189
+ puts "Y axis time #{Time.now - t}, drawing lines"
190
+ t = Time.now
182
191
  plot_axis_y_text.draw(@image)
183
- puts "#{Time.now - t} drawing text"
192
+ puts "Y axis time #{Time.now - t}, drawing text"
184
193
  end
185
194
 
186
195
  def render_parameters_axis
@@ -222,9 +231,10 @@ class GraphAxis
222
231
 
223
232
  t = Time.now
224
233
  plot_axis_x_line.draw(@image)
225
- puts "#{Time.now - t} drawing lines"
234
+ puts "X axis time #{Time.now - t}, drawing lines"
235
+ t = Time.now
226
236
  plot_axis_x_text.draw(@image)
227
- puts "#{Time.now - t} drawing text"
237
+ puts "X axis time #{Time.now - t}, drawing lines"
228
238
 
229
239
  end
230
240
 
@@ -314,7 +324,8 @@ class GraphAxis
314
324
  plot_axis_text.pointsize(options[:axis_label_font_size])
315
325
  plot_axis_text.font_family('helvetica')
316
326
  plot_axis_text.font_style(Magick::NormalStyle)
317
- plot_axis_text.text_align(Magick::LeftAlign)
327
+ #plot_axis_text.text_align(Magick::LeftAlign)
328
+ plot_axis_text.text_align(Magick::CenterAlign)
318
329
  plot_axis_text.text_undercolor(options[:background_color])
319
330
 
320
331
  plot_axis_text.text(
@@ -332,7 +343,8 @@ class GraphAxis
332
343
  plot_axis_text.pointsize(options[:axis_label_font_size])
333
344
  plot_axis_text.font_family('helvetica')
334
345
  plot_axis_text.font_style(Magick::NormalStyle)
335
- plot_axis_text.text_align(Magick::LeftAlign)
346
+ #plot_axis_text.text_align(Magick::LeftAlign)
347
+ plot_axis_text.text_align(Magick::CenterAlign)
336
348
  plot_axis_text.text_undercolor(options[:background_color])
337
349
 
338
350
  plot_axis_text = plot_axis_text.rotate(90)
@@ -170,6 +170,8 @@ class GraphImageDrawer
170
170
 
171
171
  # Render data layer
172
172
  def render_data_layer(l)
173
+ layer_data = l.processed_data
174
+
173
175
  layer_line = Magick::Draw.new
174
176
  layer_text = Magick::Draw.new
175
177
 
@@ -196,21 +198,21 @@ class GraphImageDrawer
196
198
  # calculate coords, draw text, and then lines and circles
197
199
  coords = Array.new
198
200
 
199
- (0...(l.data.size - 1)).each do |i|
200
- ax = l.data[i][:x]
201
+ (0...(layer_data.size - 1)).each do |i|
202
+ ax = layer_data[i].x
201
203
  ax = calc_bitmap_x(ax).round
202
- ay = l.data[i][:y]
204
+ ay = layer_data[i].y
203
205
  ay = calc_bitmap_y(ay).round
204
206
 
205
- bx = l.data[i+1][:x]
207
+ bx = layer_data[i+1].x
206
208
  bx = calc_bitmap_x(bx).round
207
- by = l.data[i+1][:y]
209
+ by = layer_data[i+1].y
208
210
  by = calc_bitmap_y(by).round
209
211
 
210
212
  coords << {
211
213
  :ax => ax, :ay => ay,
212
214
  :bx => bx, :by => by,
213
- :dy => l.data[i][:y]
215
+ :dy => layer_data[i].y
214
216
  }
215
217
  end
216
218
 
@@ -49,8 +49,6 @@ class TechnicalGraph
49
49
  @axis.render_on_image(@image)
50
50
  # draw layers
51
51
  @layers.each do |l|
52
- # external processing
53
- l.process
54
52
  # drawing
55
53
  @image_drawer.render_data_layer(l)
56
54
  end