technical_graph 0.3.0 → 0.3.1

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