morphological_metrics 1.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.
data/lib/mm/ratio.rb ADDED
@@ -0,0 +1,148 @@
1
+ require 'yaml'
2
+ require 'prime'
3
+
4
+ module MM; end
5
+
6
+ class MM::Ratio
7
+ include Comparable
8
+ include Enumerable
9
+
10
+ def initialize n, d
11
+ gcd = n.gcd d
12
+ @numerator = n / gcd
13
+ @denominator = d / gcd
14
+ end
15
+
16
+ attr_accessor :numerator, :denominator
17
+
18
+ def * other
19
+ MM::Ratio.new(self.numerator * other.numerator, self.denominator * other.denominator)
20
+ end
21
+
22
+ def / other
23
+ self * other.reciprocal
24
+ end
25
+
26
+ def + other
27
+ MM::Ratio.new(self.numerator*other.denominator + other.numerator*self.denominator,
28
+ self.denominator*other.denominator)
29
+ end
30
+
31
+ def - other
32
+ self + (other * MM::Ratio.new(-1,1))
33
+ end
34
+
35
+ # Works very similarly to the Prime::prime_division method, except that
36
+ # factors in the numerator are positive, and factors in the denominator are
37
+ # negative.
38
+ def factors
39
+ n_factors = ::Prime.prime_division(@numerator)
40
+ d_factors = ::Prime.prime_division(@denominator).map {|d| d[1] *= -1; d}
41
+ n_factors.concat(d_factors).sort_by {|x| x[0]}
42
+ end
43
+
44
+ def abs
45
+ if self < MM::Ratio.new(0, 1)
46
+ self * MM::Ratio.new(-1,1)
47
+ else
48
+ self
49
+ end
50
+ end
51
+
52
+ def <=> other
53
+ # Ensure that the comparison makes sense
54
+ return nil unless other.respond_to? :-
55
+
56
+ case
57
+ when (self - other).to_f > 0
58
+ return 1
59
+ when (self - other).to_f < 0
60
+ return -1
61
+ end
62
+ return 0
63
+ end
64
+
65
+ def eql? other
66
+ other.is_a?(MM::Ratio) && (self == other)
67
+ end
68
+
69
+ def hash
70
+ [@numerator, @denominator, MM::Ratio].hash
71
+ end
72
+
73
+ def cents
74
+ Math.log2(self.to_f) * 1200.0
75
+ end
76
+
77
+ def self.from_s r
78
+ if r.respond_to? :split
79
+ if r =~ /\s/
80
+ r.split(/\s/).inject([]) {|memo, ratio|
81
+ memo << self.from_s(ratio)
82
+ }
83
+ else
84
+ string_to_ratio r
85
+ end
86
+ else
87
+ r.map {|s| self.from_s s}
88
+ end
89
+ end
90
+
91
+ def self.string_to_ratio string
92
+ m = string.match(/(\d+)\/(\d+)/)
93
+ MM::Ratio.new(m[1].to_i, m[2].to_i)
94
+ end
95
+
96
+ # Loads a sequence of MM::Ratios from a YAML file.
97
+ def self.from_yaml yaml_string
98
+ YAML.load(yaml_string).map {|r| MM::Ratio.from_s r}
99
+ end
100
+
101
+ def to_f
102
+ @numerator.to_f / @denominator
103
+ end
104
+
105
+ def to_s
106
+ "#{@numerator}/#{@denominator}"
107
+ end
108
+
109
+ def reciprocal
110
+ MM::Ratio.new(@denominator, @numerator)
111
+ end
112
+
113
+ def prime_limit
114
+ self.map { |r|
115
+ r.prime_division.map { |s|
116
+ s.first
117
+ }.max
118
+ }.compact.max
119
+ end
120
+
121
+ def each
122
+ if block_given?
123
+ [@numerator, @denominator].each do |r|
124
+ yield r
125
+ end
126
+ else
127
+ [@numerator, @denominator].each
128
+ end
129
+ end
130
+
131
+ def self.to_vector point
132
+ point.each_cons(2).map {|r| r[0] / r[1]}
133
+ end
134
+
135
+ def self.from_vector vector
136
+ vector.inject([MM::Ratio.new(1,1)]) {|m, r| m << (m.last / r)}
137
+ end
138
+
139
+ def self.change_interval point, index, interval
140
+ vector = MM::Ratio.to_vector(point)
141
+ if interval == :reciprocal
142
+ interval = vector[index].reciprocal
143
+ end
144
+ vector[index] = interval
145
+ MM::Ratio.from_vector(vector)
146
+ end
147
+ end
148
+
data/lib/mm/scaling.rb ADDED
@@ -0,0 +1,27 @@
1
+ module MM
2
+ module Scaling
3
+ # All of these functions require a sequence of distance pair evaluations of
4
+ # the given metric. They all output scaled versions.
5
+
6
+ # Scale to the max across both vector
7
+ def self.none pairs
8
+ pairs
9
+ end
10
+ def self.absolute pairs
11
+ max = (pairs.map(&:max)).max
12
+ pairs.map {|x| x.map {|y| y.to_f / max}}
13
+ end
14
+ # Scale each vector to its own max
15
+ def self.relative pairs
16
+ maxes = pairs.map(&:max)
17
+ pairs.zip(maxes).map {|pair, max| pair.map {|x| x.to_f / max}}
18
+ end
19
+ # Note: a bit hacky. But anything starting with "get_" should be considered
20
+ # a meta-scaling method. This method returns a Proc that has a particular
21
+ # scaling value hard-coded into it, for re-use and re-use.
22
+ def self.get_global max
23
+ ->(pairs) {pairs.map {|x| x.map {|y| y.to_f / max}}}
24
+ end
25
+ end
26
+ end
27
+
data/lib/mm/search.rb ADDED
@@ -0,0 +1,97 @@
1
+ module MM; end
2
+
3
+ # All you need to do is add an adjacent_points_function and cost_function
4
+ class MM::Search
5
+ attr_accessor :candidates, :delta, :starting_point, :path, :banned, :iterations
6
+ attr_writer :cost_function, :adjacent_points_function
7
+
8
+ def initialize starting_point, delta = 0.001
9
+ @starting_point = starting_point
10
+ @delta = delta
11
+ @current_point = @starting_point
12
+ @path = []
13
+ @banned = []
14
+ @iterations = 0
15
+ end
16
+
17
+ # Finds a vector beginning from the starting point
18
+ def find
19
+ find_from_point @starting_point
20
+ end
21
+
22
+ # def find_from_point point
23
+ # if cost_function(point) < current_cost
24
+ # add_to_path point
25
+ # if made_it?
26
+ # return @current_point
27
+ # else
28
+ # get_sorted_adjacent_points.each do |possible_point|
29
+ # find_from_point possible_point
30
+ # end
31
+ # end
32
+ # else
33
+ # backtrack
34
+ # end
35
+ # end
36
+
37
+
38
+ def find_from_point point
39
+ @iterations += 1
40
+ # The adjacent points are all sorted
41
+ # raise StopIteration if cost_function(point) > current_cost
42
+ add_to_path point
43
+ sorted_adjacent_points = get_sorted_adjacent_points
44
+ # If we've made it, return it.
45
+ if made_it?
46
+ @current_point
47
+ else
48
+ begin
49
+ find_from_point sorted_adjacent_points.next
50
+ rescue StopIteration => er
51
+ # When the list of adjacent points runs out, backtrack
52
+ backtrack
53
+ retry unless @current_point.nil?
54
+ end
55
+ end
56
+ end
57
+
58
+ def add_to_path point
59
+ @current_point = point
60
+ @path << point
61
+ end
62
+
63
+ def backtrack
64
+ @banned << @path.pop
65
+ puts "Path: #{@path.size}, Banned: #{@banned.size}" if ENV["DEBUG_RB"] && ENV["DEBUG_RB"].to_i > 1
66
+ @current_point = @path.last
67
+ end
68
+
69
+ def calculate_cost candidates
70
+ candidates.map {|x| cost_function x}
71
+ end
72
+
73
+ def made_it?
74
+ current_cost < @delta
75
+ end
76
+
77
+ def cost_function *args
78
+ @cost_function.call(*args)
79
+ end
80
+
81
+ def current_cost
82
+ cost_function @current_point
83
+ end
84
+
85
+ def get_adjacent_points *args
86
+ @adjacent_points_function.call(@current_point, *args)
87
+ end
88
+
89
+ def get_sorted_adjacent_points *args
90
+ get_adjacent_points(*args)
91
+ .reject {|c| @path.include? c}
92
+ .reject {|c| @banned.include? c}
93
+ .sort_by {|x| cost_function x}
94
+ .to_enum
95
+ end
96
+ end
97
+
data/lib/mm.rb ADDED
@@ -0,0 +1,11 @@
1
+ module MM
2
+ VERSION = "1.1.0"
3
+ end
4
+
5
+ require 'mm/deltas'
6
+ require 'mm/metric'
7
+ require 'mm/pairs'
8
+ require 'mm/ratio'
9
+ require 'mm/scaling'
10
+ require 'mm/search'
11
+
data/lib/shortcuts.yml ADDED
@@ -0,0 +1,49 @@
1
+ :olm:
2
+ :ordered: true
3
+ :pair: :linear
4
+ :scale: :none
5
+ :intra_delta: :abs
6
+ :inter_delta: :abs
7
+ :ocm:
8
+ :ordered: true
9
+ :pair: :combinatorial
10
+ :scale: :none
11
+ :intra_delta: :abs
12
+ :inter_delta: :abs
13
+ :ulm:
14
+ :ordered: false
15
+ :pair: :linear
16
+ :scale: :none
17
+ :intra_delta: :abs
18
+ :inter_delta: :mean
19
+ :ucm:
20
+ :ordered: false
21
+ :pair: :combinatorial
22
+ :scale: :none
23
+ :intra_delta: :abs
24
+ :inter_delta: :mean
25
+ :old:
26
+ :ordered: true
27
+ :pair: :linear
28
+ :scale: :none
29
+ :intra_delta: :direction
30
+ :inter_delta: :abs
31
+ :ocd:
32
+ :ordered: true
33
+ :pair: :combinatorial
34
+ :scale: :none
35
+ :intra_delta: :direction
36
+ :inter_delta: :abs
37
+ :uld:
38
+ :ordered: false
39
+ :pair: :linear
40
+ :scale: :none
41
+ :intra_delta: :direction
42
+ :inter_delta: :mean
43
+ :ucd:
44
+ :ordered: false
45
+ :pair: :combinatorial
46
+ :scale: :none
47
+ :intra_delta: :direction
48
+ :inter_delta: :mean
49
+
data/test/helpers.rb ADDED
@@ -0,0 +1,22 @@
1
+ module TestHelpers
2
+ # Asserts that each item in exp matches each item in act
3
+ def assert_nested_in_delta exp, act, delta = 0.001, msg = nil
4
+ exp.zip(act) do |x|
5
+ if block_given?
6
+ yield x, delta, msg
7
+ else
8
+ assert_in_delta(*x, delta, msg)
9
+ end
10
+ end
11
+ end
12
+
13
+ # Asserts that nested values 2-deep are within a certain delta
14
+ def assert_nested_in_delta_2_deep *args
15
+ assert_nested_in_delta(*args) do |x, delta, msg|
16
+ x[0].zip(x[1]) do |y|
17
+ assert_in_delta(*y, delta, msg)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,43 @@
1
+ require 'mm/deltas'
2
+ require_relative '../helpers.rb'
3
+
4
+ class TestMM < Minitest::Test; end
5
+
6
+ class TestMM::TestDeltas < Minitest::Test
7
+ include TestHelpers
8
+
9
+ def test_abs_ordered_inter_delta
10
+ input = [[5, 12], [4, 2], [3, 11], [6, 7]]
11
+ exp = [7, 2, 8, 1]
12
+ result = input.map {|x| MM::Deltas.abs(x)}
13
+ assert_equal exp, result
14
+ end
15
+ def test_mean_inter_delta
16
+ input = [[5, 4, 3, 6], [12, 2, 11, 7]]
17
+ exp = [4.5, 8]
18
+ result = input.map {|x| MM::Deltas.mean(x)}
19
+ assert_equal exp, result
20
+ end
21
+ def test_distance_ordered_inter_delta
22
+ input = [[-1, -1], [1, 1], [-1, 1], [-1, -1]]
23
+ exp = [0, 0, 2, 0]
24
+ result = input.map {|x| MM::Deltas.abs(x)}
25
+ assert_equal exp, result
26
+ end
27
+ def test_tenney_ordered_intra_delta
28
+ input = [[Rational(1,1), Rational(3,2)], [Rational(3,2), Rational(5,4)]]
29
+ exp = [2.585, 4.907]
30
+ assert_nested_in_delta exp, input.map {|x| MM::Deltas.tenney(x)}
31
+ end
32
+ def test_log_ratio_ordered_intra_delta
33
+ input = [[Rational(1,1), Rational(3,2)], [Rational(3,2), Rational(5,4)]]
34
+ exp = [0.585, 0.263]
35
+ assert_nested_in_delta exp, input.map {|x| MM::Deltas.log_ratio(x)}
36
+ end
37
+ def test_ratio_ordered_intra_delta
38
+ input = [[Rational(1,1), Rational(3,2)], [Rational(3,2), Rational(5,4)]]
39
+ exp = [Rational(2,3), Rational(6,5)]
40
+ assert_nested_in_delta exp, input.map {|x| MM::Deltas.ratio(x)}
41
+ end
42
+ end
43
+
@@ -0,0 +1,168 @@
1
+ require 'minitest/autorun'
2
+
3
+ require 'mm/metric'
4
+
5
+ class TestMM < Minitest::Test; end
6
+
7
+ class TestMM::TestMetric < Minitest::Test
8
+ def setup
9
+ @ordered ||= true
10
+ @pair ||= :linear
11
+ @scale ||= :none
12
+ @intra_delta ||= :abs
13
+ @inter_delta ||= :abs
14
+ @metric = MM::Metric.new(ordered: @ordered, pair: @pair, scale: @scale, intra_delta: @intra_delta, inter_delta: @inter_delta)
15
+
16
+ # Setting up the sample vectors for many of the examples
17
+ @v1 = [1, 6, 2, 5, 11]
18
+ @v2 = [3, 15, 13, 2, 9]
19
+ end
20
+
21
+ def test_creates_new_metric
22
+ assert @metric.is_a? MM::Metric
23
+ end
24
+
25
+ def test_metric_responds_to_call
26
+ assert_respond_to @metric, :call
27
+ end
28
+
29
+ class TestGetPairs < self
30
+ class TestLinearPairs < self
31
+ def test_gets_linear_pairs
32
+ exp = [
33
+ [[1, 6], [6, 2], [2, 5], [5, 11]],
34
+ [[3, 15], [15, 13], [13, 2], [2, 9]]
35
+ ]
36
+ assert_equal exp, @metric.send(:get_pairs, @v1, @v2)
37
+ end
38
+ end
39
+
40
+ class TestCombinatorialPairs < self
41
+ def setup
42
+ @pair = :combinatorial
43
+ super
44
+ end
45
+ def test_gets_combinatorial_pairs
46
+ exp = [
47
+ [[1, 6], [1, 2], [1, 5], [1, 11], [6, 2], [6, 5],
48
+ [6, 11], [2, 5], [2, 11], [5, 11]],
49
+ [[3, 15], [3, 13], [3, 2], [3, 9], [15, 13], [15, 2],
50
+ [15, 9], [13, 2], [13, 9], [2, 9]]
51
+ ]
52
+ assert_equal exp, @metric.send(:get_pairs, @v1, @v2)
53
+ end
54
+ end
55
+ end
56
+
57
+ class TestDeltas < self
58
+ class TestIntraDelta < self
59
+ def test_gets_intra_delta
60
+ pairs = [
61
+ [[1, 6], [6, 2], [2, 5], [5, 11]],
62
+ [[3, 15], [15, 13], [13, 2], [2, 9]]
63
+ ]
64
+ exp = [
65
+ [5, 4, 3, 6],
66
+ [12, 2, 11, 7]
67
+ ]
68
+ assert_equal exp, @metric.send(:intra_delta, pairs)
69
+ end
70
+ def test_intra_delta_proc
71
+ @metric.intra_delta = ->(*vp) {nil}
72
+ assert_instance_of Proc, @metric.instance_variable_get(:@intra_delta)
73
+ end
74
+ end
75
+
76
+ class TestInterDelta < self
77
+ def setup
78
+ super
79
+ @diffs = [
80
+ [5, 4, 3, 6],
81
+ [12, 2, 11, 7]
82
+ ]
83
+ end
84
+ def test_gets_inter_delta_ordered
85
+ exp = 4.5
86
+ assert_equal exp, @metric.send(:inter_delta, @diffs)
87
+ end
88
+ def test_inter_delta_proc
89
+ @metric.inter_delta = ->(*diffs) {nil}
90
+ assert_instance_of Proc, @metric.instance_variable_get(:@inter_delta)
91
+ end
92
+ end
93
+ end
94
+
95
+ class TestScaling < self
96
+ def setup
97
+ super
98
+ @unscaled = [
99
+ [5, 4, 3, 6],
100
+ [12, 2, 11, 7]
101
+ ]
102
+ end
103
+
104
+ def test_assigns_scaling_proc
105
+ @metric.scale = ->(pairs) {}
106
+ scale = @metric.instance_variable_get :@scale
107
+ assert_equal Proc, scale.class
108
+ end
109
+
110
+ def test_no_scaling
111
+ assert_equal @unscaled, @metric.send(:scale, @unscaled)
112
+ end
113
+
114
+ # TODO: This is a complicated test and I don't like it
115
+ def test_absolute_scaling
116
+ @metric.scale = :absolute
117
+ @exp = [
118
+ [0.417, 0.333, 0.25, 0.5],
119
+ [1.0, 0.167, 0.917, 0.583]
120
+ ]
121
+ actual = @metric.send :scale, @unscaled
122
+ actual.each_with_index do |v, i|
123
+ v.each_with_index do |w, j|
124
+ assert_in_delta @exp[i][j], w, 0.001
125
+ end
126
+ end
127
+ end
128
+
129
+ def test_relative_scaling
130
+ @metric.scale = :relative
131
+ @exp = [
132
+ [0.833, 0.667, 0.5, 1.0],
133
+ [1.0, 0.167, 0.917, 0.583]
134
+ ]
135
+ actual = @metric.send :scale, @unscaled
136
+ actual.each_with_index do |v, i|
137
+ v.each_with_index do |w, j|
138
+ assert_in_delta @exp[i][j], w, 0.001
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ class TestMagnitudeMetric < self
145
+ # Definitions of expected results
146
+ @exp = {
147
+ :olm => {:scale_none => 4.5, :scale_absolute => 0.375},
148
+ :ocm => {:scale_none => 5.2, :scale_absolute => 0.4},
149
+ :ulm => {:scale_none => 3.5, :scale_absolute => 0.29167},
150
+ :ucm => {:scale_none => 2.4, :scale_absolute => 0.1846},
151
+ :old => {:scale_none => 0.25},
152
+ :ocd => {:scale_none => 0.4},
153
+ :uld => {:scale_none => 0.25},
154
+ :ucd => {:scale_none => 0.4}
155
+ }
156
+
157
+ @exp.each do |metric, expected|
158
+ expected.each do |scaling, e|
159
+ define_method "test_#{metric}_#{scaling}" do
160
+ m = ::MM::Metric.send(metric)
161
+ m.scale = /_(.*)$/.match(scaling)[1].to_sym
162
+ assert_in_delta e, m.call(@v1, @v2), 0.001
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+
@@ -0,0 +1,6 @@
1
+ require "minitest/autorun"
2
+ require "mm"
3
+
4
+ class TestMM < Minitest::Test
5
+
6
+ end
@@ -0,0 +1,27 @@
1
+ # require "minitest/autorun"
2
+ require "mm/pairs"
3
+
4
+ class TestMM < Minitest::Test; end
5
+
6
+ class TestMM::TestPairs < Minitest::Test
7
+ def setup
8
+ @pairs = MM::Pairs.new
9
+ end
10
+
11
+ def test_linear_flat_array
12
+ assert_equal [[0, 1], [1, 2], [2, 3]], @pairs.linear([0, 1, 2, 3])
13
+ end
14
+
15
+ def test_linear_nested_array
16
+ assert_equal [[[0, 1], [2, 3]], [[2, 3], [4, 5]]], @pairs.linear([[0, 1], [2, 3], [4, 5]])
17
+ end
18
+
19
+ def test_combinatorial_flat_array
20
+ assert_equal [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]], @pairs.combinatorial([0, 1, 2, 3])
21
+ end
22
+
23
+ def test_combinatorial_nested_array
24
+ assert_equal [[[0, 1], [2, 3]], [[0, 1], [4, 5]], [[2, 3], [4, 5]]], @pairs.combinatorial([[0, 1], [2, 3], [4, 5]])
25
+ end
26
+ end
27
+