mhl 0.2.0 → 0.3.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,142 @@
1
+ module MHL
2
+
3
+ # This class implements a genotype with real space representation
4
+ class RealVectorGenotypeSpace
5
+ def initialize(opts, logger)
6
+ @random_func = opts[:random_func]
7
+
8
+ @dimensions = opts[:dimensions].to_i
9
+ unless @dimensions and @dimensions > 0
10
+ raise ArgumentError, 'The number of dimensions must be a positive integer!'
11
+ end
12
+
13
+ # TODO: enable to choose which recombination function to use
14
+ case opts[:recombination_type].to_s
15
+ when /intermediate/i
16
+ @recombination_func = :extended_intermediate_recombination
17
+ when /line/i
18
+ @recombination_func = :extended_line_recombination
19
+ else
20
+ raise ArgumentError, 'Recombination function must be either line or intermediate!'
21
+ end
22
+
23
+ @constraints = opts[:constraints]
24
+ if !@constraints or @constraints.size != @dimensions
25
+ raise ArgumentError, 'Real-valued GA variants require constraints!'
26
+ end
27
+
28
+ @logger = logger
29
+ end
30
+
31
+ def get_random
32
+ if @random_func
33
+ @random_func.call
34
+ else
35
+ if @constraints
36
+ @constraints.map{|x| x[:from] + SecureRandom.random_number(x[:to] - x[:from]) }
37
+ else
38
+ raise 'Automated random genotype generation when no constraints are provided is not implemented yet!'
39
+ end
40
+ end
41
+ end
42
+
43
+ # reproduction with power mutation and line or intermediate recombination
44
+ def reproduce_from(p1, p2, mutation_rv, recombination_rv)
45
+ # make copies of p1 and p2
46
+ # (we're only interested in the :genotype key)
47
+ c1 = { genotype: p1[:genotype].dup }
48
+ c2 = { genotype: p2[:genotype].dup }
49
+
50
+ # mutation comes first
51
+ power_mutation(c1[:genotype], mutation_rv)
52
+ power_mutation(c2[:genotype], mutation_rv)
53
+
54
+ # and then recombination
55
+ send(@recombination_func, c1[:genotype], c2[:genotype], recombination_rv)
56
+
57
+ if @constraints
58
+ repair_chromosome(c1[:genotype])
59
+ repair_chromosome(c2[:genotype])
60
+ end
61
+
62
+ return c1, c2
63
+ end
64
+
65
+
66
+ private
67
+
68
+ # power mutation [DEEP07]
69
+ # NOTE: this mutation operator won't work unless constraints are given
70
+ def power_mutation(parent, mutation_rv)
71
+ s = mutation_rv.next ** 10.0
72
+
73
+ min = @constraints.map{|x| x[:from] }
74
+ max = @constraints.map{|x| x[:to] }
75
+
76
+ parent.each_index do |i|
77
+ t_i = (parent[i] - min[i]) / (max[i]-min[i])
78
+
79
+ if rand() >= t_i
80
+ # sometimes the variation will be positive ...
81
+ parent[i] += s * (max[i] - parent[i])
82
+ else
83
+ # ... and sometimes it will be negative
84
+ parent[i] -= s * (parent[i] - min[i])
85
+ end
86
+ end
87
+ end
88
+
89
+ # extended intermediate recombination [MUHLENBEIN93] (see [LUKE15] page 42)
90
+ def extended_intermediate_recombination(g1, g2, recombination_rv)
91
+ # TODO: disable this check in non-debugging mode
92
+ raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
93
+
94
+ # recombination
95
+ g1.each_index do |i|
96
+ begin
97
+ alpha = recombination_rv.next
98
+ beta = recombination_rv.next
99
+ t = alpha * g1[i] + (1.0 - alpha) * g2[i]
100
+ s = beta * g2[i] + (1.0 - beta) * g1[i]
101
+ end
102
+ g1[i] = t
103
+ g2[i] = s
104
+ end
105
+ end
106
+
107
+ # extended line recombination [MUHLENBEIN93] (see [LUKE15] page 42)
108
+ def extended_line_recombination(g1, g2, recombination_rv)
109
+ # TODO: disable this check in non-debugging mode
110
+ raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
111
+
112
+ alpha = recombination_rv.next
113
+ beta = recombination_rv.next
114
+
115
+ # recombination
116
+ g1.each_index do |i|
117
+ t = alpha * g1[i] + (1.0 - alpha) * g2[i]
118
+ s = beta * g2[i] + (1.0 - beta) * g1[i]
119
+ g1[i] = t
120
+ g2[i] = s
121
+ end
122
+ end
123
+
124
+ def repair_chromosome(g)
125
+ g.each_index do |i|
126
+ if g[i] < @constraints[i][:from]
127
+ range = "[#{@constraints[i][:from]},#{@constraints[i][:to]}]"
128
+ @logger.debug "repairing g[#{i}] #{g[i]} to fit within #{range}" if @logger
129
+ g[i] = @constraints[i][:from]
130
+ @logger.debug "g[#{i}] repaired as: #{g[i]}" if @logger
131
+ elsif g[i] > @constraints[i][:to]
132
+ range = "[#{@constraints[i][:from]},#{@constraints[i][:to]}]"
133
+ @logger.debug "repairing g[#{i}] #{g[i]} to fit within #{range}" if @logger
134
+ g[i] = @constraints[i][:to]
135
+ @logger.debug "g[#{i}] repaired as: #{g[i]}" if @logger
136
+ end
137
+ end
138
+ end
139
+
140
+ end
141
+
142
+ end
@@ -0,0 +1,49 @@
1
+ module MHL
2
+ class RechenbergController
3
+
4
+ DEFAULT_THRESHOLD = 1.0/5.0
5
+ TAU = 1.10
6
+ P_M_MAX = 0.99
7
+ P_M_MIN = 0.01
8
+
9
+ attr_reader :threshold, :generations
10
+
11
+ def initialize(generations=5, threshold=DEFAULT_THRESHOLD, logger=nil)
12
+ unless threshold > 0.0 and threshold < 1.0
13
+ raise ArgumentError, "The threshold parameter must be in the (0.0,1.0) range!"
14
+ end
15
+ @generations = generations
16
+ @threshold = threshold
17
+ @logger = logger
18
+ @history = []
19
+ end
20
+
21
+ def call(solver, best)
22
+ @history << best
23
+
24
+ if @history.size > @generations
25
+ # calculate improvement ratio
26
+ res = @history.each_cons(2).inject(0) {|s,x| s += 1 if x[1][:fitness] > x[0][:fitness]; s } / (@history.size - 1).to_f
27
+ if res > @threshold
28
+ # we had enough improvements - decrease impact of mutation
29
+ # increase mutation probability by 5% or to P_M_MAX
30
+ old_p_m = solver.mutation_probability
31
+ new_p_m = [ old_p_m * TAU, P_M_MAX ].min
32
+ @logger.info "increasing mutation_probability from #{old_p_m} to #{new_p_m}" if @logger
33
+ solver.mutation_probability = new_p_m
34
+ else
35
+ # we didn't have enough improvements - increase impact of mutation
36
+ # decrease mutation probability by 5% or to P_M_MAX
37
+ old_p_m = solver.mutation_probability
38
+ new_p_m = [ old_p_m / TAU, P_M_MIN ].max
39
+ @logger.info "decreasing mutation_probability from #{old_p_m} to #{new_p_m}" if @logger
40
+ solver.mutation_probability = new_p_m
41
+ end
42
+
43
+ # reset
44
+ @history.shift(@history.size-1)
45
+ end
46
+ end
47
+ end
48
+
49
+ end
@@ -1,3 +1,3 @@
1
1
  module MHL
2
- VERSION = '0.2.0'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -20,8 +20,11 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_dependency 'bitstring'
22
22
  spec.add_dependency 'concurrent-ruby', '~> 1.0'
23
- spec.add_dependency 'erv'
23
+ spec.add_dependency 'erv', '>= 0.3.4'
24
24
 
25
25
  spec.add_development_dependency 'bundler'
26
+ spec.add_development_dependency 'dotenv', '~> 2.5.0'
26
27
  spec.add_development_dependency 'rake'
28
+ spec.add_development_dependency 'minitest'
29
+ spec.add_development_dependency 'minitest-spec-context', '~> 0.0.3'
27
30
  end
@@ -2,51 +2,89 @@ require 'test_helper'
2
2
 
3
3
  describe MHL::GeneticAlgorithmSolver do
4
4
 
5
+ let :logger do
6
+ :stderr
7
+ end
8
+
9
+ let :log_level do
10
+ ENV['DEBUG'] ? :debug : :warn
11
+ end
12
+
5
13
  it 'should accept bitstring representation genotypes' do
6
14
  MHL::GeneticAlgorithmSolver.new(
7
- :population_size => 128,
8
- :genotype_space_type => :bitstring,
9
- :mutation_threshold => 0.5,
10
- :recombination_threshold => 0.5,
11
- :genotype_space_conf => {
12
- :bitstring_length => 120,
15
+ population_size: 128,
16
+ genotype_space_type: :bitstring,
17
+ mutation_threshold: 0.5,
18
+ recombination_threshold: 0.5,
19
+ genotype_space_conf: {
20
+ bitstring_length: 120,
13
21
  },
14
- :logger => :stderr,
15
- :log_level => ENV['DEBUG'] ? Logger::DEBUG : Logger::WARN,
22
+ logger: logger,
23
+ log_level: log_level,
16
24
  )
17
25
  end
18
26
 
19
27
  it 'should accept integer representation genotypes' do
20
28
  MHL::GeneticAlgorithmSolver.new(
21
- :population_size => 128,
22
- :genotype_space_type => :integer,
23
- :mutation_probability => 0.5,
24
- :recombination_probability => 0.5,
25
- :genotype_space_conf => {
26
- :dimensions => 6,
27
- :recombination_type => :intermediate,
29
+ population_size: 128,
30
+ genotype_space_type: :integer,
31
+ mutation_probability: 0.5,
32
+ recombination_probability: 0.5,
33
+ genotype_space_conf: {
34
+ dimensions: 6,
35
+ recombination_type: :intermediate,
28
36
  },
29
- :logger => :stderr,
30
- :log_level => ENV['DEBUG'] ? Logger::DEBUG : Logger::WARN,
37
+ logger: logger,
38
+ log_level: log_level,
31
39
  )
32
40
  end
33
41
 
34
- it 'should solve a 2-dimension parabola in integer space' do
35
- solver = MHL::GeneticAlgorithmSolver.new(
36
- :population_size => 40,
37
- :genotype_space_type => :integer,
38
- :mutation_probability => 0.5,
39
- :recombination_probability => 0.5,
40
- :genotype_space_conf => {
41
- :dimensions => 2,
42
- :recombination_type => :intermediate,
43
- :random_func => lambda { Array.new(2) { rand(20) } }
42
+ let :solver do
43
+ MHL::GeneticAlgorithmSolver.new(
44
+ population_size: 40,
45
+ genotype_space_type: :integer,
46
+ mutation_probability: 0.5,
47
+ recombination_probability: 0.5,
48
+ genotype_space_conf: {
49
+ dimensions: 2,
50
+ recombination_type: :intermediate,
51
+ random_func: lambda { Array.new(2) { rand(20) } }
44
52
  },
45
- :exit_condition => lambda {|generation,best_sample| best_sample[:fitness] == 0},
46
- :logger => :stderr,
47
- :log_level => ENV['DEBUG'] ? Logger::DEBUG : Logger::WARN,
53
+ exit_condition: lambda {|generation,best_sample| best_sample[:fitness] == 0},
54
+ logger: logger,
55
+ log_level: log_level,
48
56
  )
49
- solver.solve(Proc.new{|genotype| -(genotype[0]**2 + genotype[1]**2) })
57
+ end
58
+
59
+ context 'concurrent' do
60
+
61
+ it 'should solve a thread-safe function concurrently' do
62
+ func = -> position do
63
+ -(position.inject(0.0) {|s,x| s += x**2 })
64
+ end
65
+
66
+ solver.solve(func, concurrent: true)
67
+ end
68
+
69
+ end
70
+
71
+ context 'sequential' do
72
+
73
+ it 'should solve a non-thread safe function sequentially' do
74
+ # here we create a specially modified version of the function to optimize
75
+ # that raises an error if called concurrently
76
+ mx = Mutex.new
77
+ func = -> position do
78
+ raise "Sequential call check failed" if mx.locked?
79
+ mx.synchronize do
80
+ sleep 0.005
81
+ -(position.inject(0.0) {|s,x| s += x**2 })
82
+ end
83
+ end
84
+
85
+ solver.solve(func)
86
+ end
87
+
50
88
  end
51
89
 
52
90
  end
@@ -1,19 +1,22 @@
1
1
  require 'test_helper'
2
2
 
3
3
  describe MHL::IntegerVectorGenotypeSpace do
4
+
4
5
  let :logger do
5
- l = Logger.new(STDOUT)
6
- l.level = ENV['DEBUG'] ? Logger::DEBUG : Logger::WARN
6
+ l = Logger.new(STDERR)
7
+ l.level = ENV['DEBUG'] ? :debug : :warn
7
8
  l
8
9
  end
9
10
 
10
11
  it 'should refuse to work with non-positive dimensions' do
11
12
  assert_raises(ArgumentError) do
12
13
  MHL::IntegerVectorGenotypeSpace.new(
13
- :dimensions => -rand(100),
14
- :recombination_type => :intermediate,
15
- :logger => logger,
16
- # :random_func => lambda { Array.new(2) { rand(20) } }
14
+ {
15
+ dimensions: -rand(100),
16
+ recombination_type: :intermediate,
17
+ # random_func: lambda { Array.new(2) { rand(20) } }
18
+ },
19
+ logger
17
20
  )
18
21
  end
19
22
  end
@@ -21,9 +24,11 @@ describe MHL::IntegerVectorGenotypeSpace do
21
24
  it 'should refuse to work with non- line or intermediate recombination' do
22
25
  assert_raises(ArgumentError) do
23
26
  MHL::IntegerVectorGenotypeSpace.new(
24
- :dimensions => 2,
25
- :recombination_type => :something,
26
- :logger => logger,
27
+ {
28
+ dimensions: 2,
29
+ recombination_type: :something,
30
+ },
31
+ logger
27
32
  )
28
33
  end
29
34
  end
@@ -33,11 +38,13 @@ describe MHL::IntegerVectorGenotypeSpace do
33
38
  x1 = rand(100); x2 = x1 + rand(200)
34
39
  y1 = -rand(100); y2 = y1 + rand(200)
35
40
  is = MHL::IntegerVectorGenotypeSpace.new(
36
- :dimensions => 2,
37
- :recombination_type => :intermediate,
38
- :constraints => [ { :from => x1, :to => x2 },
39
- { :from => y1, :to => y2 } ],
40
- :logger => logger,
41
+ {
42
+ dimensions: 2,
43
+ recombination_type: :intermediate,
44
+ constraints: [ { from: x1, to: x2 },
45
+ { from: y1, to: y2 } ],
46
+ },
47
+ logger
41
48
  )
42
49
  genotype = is.get_random
43
50
  genotype.size.must_equal 2
@@ -51,21 +58,22 @@ describe MHL::IntegerVectorGenotypeSpace do
51
58
  x1 = rand(100); x2 = x1 + rand(200)
52
59
  y1 = -rand(100); y2 = y1 + rand(200)
53
60
  is = MHL::IntegerVectorGenotypeSpace.new(
54
- :dimensions => 2,
55
- :recombination_type => :intermediate,
56
- :constraints => [ { :from => x1, :to => x2 },
57
- { :from => y1, :to => y2 } ],
58
- :logger => logger,
61
+ {
62
+ dimensions: 2,
63
+ recombination_type: :intermediate,
64
+ constraints: [ { from: x1, to: x2 },
65
+ { from: y1, to: y2 } ],
66
+ },
67
+ logger
59
68
  )
60
- g1 = { :genotype => [ x1, y1 ] }
61
- g2 = { :genotype => [ x2, y2 ] }
69
+ g1 = { genotype: [ x1, y1 ] }
70
+ g2 = { genotype: [ x2, y2 ] }
62
71
  a, b = is.reproduce_from(
63
72
  g1, g2,
64
- ERV::RandomVariable.new(:distribution => :geometric,
65
- :probability_of_success => 0.05),
66
- ERV::RandomVariable.new(:distribution => :uniform,
67
- :min_value => -0.25,
68
- :max_value => 1.25)
73
+ ERV::RandomVariable.new(distribution: :geometric,
74
+ args: { probability_of_success: 0.05 }),
75
+ ERV::RandomVariable.new(distribution: :uniform,
76
+ args: { min_value: -0.25, max_value: 1.25 })
69
77
  )
70
78
  a[:genotype][0].must_be :>=, x1
71
79
  a[:genotype][0].must_be :<=, x2
@@ -2,18 +2,56 @@ require 'test_helper'
2
2
 
3
3
  describe MHL::MultiSwarmQPSOSolver do
4
4
 
5
- it 'should solve a 2-dimension parabola in real space' do
6
- solver = MHL::MultiSwarmQPSOSolver.new(
7
- :num_swarms => 4,
8
- :constraints => {
9
- :min => [ -100, -100 ],
10
- :max => [ 100, 100 ],
5
+ let :logger do
6
+ :stderr
7
+ end
8
+
9
+ let :log_level do
10
+ ENV['DEBUG'] ? :debug : :warn
11
+ end
12
+
13
+ let :solver do
14
+ MHL::MultiSwarmQPSOSolver.new(
15
+ num_swarms: 4,
16
+ constraints: {
17
+ min: [ -100, -100, -100, -100, -100 ],
18
+ max: [ 100, 100, 100, 100, 100 ],
11
19
  },
12
- :exit_condition => lambda {|iteration,best| best[:height].abs < 0.001 },
13
- :logger => :stdout,
14
- :log_level => ENV['DEBUG'] ? Logger::DEBUG : Logger::WARN,
20
+ exit_condition: lambda {|iteration,best| best[:height].abs < 0.001 },
21
+ logger: logger,
22
+ log_level: log_level,
15
23
  )
16
- solver.solve(Proc.new{|position| -(position[0]**2 + position[1]**2) })
24
+ end
25
+
26
+ context 'concurrent' do
27
+
28
+ it 'should solve a thread-safe function concurrently' do
29
+ func = -> position do
30
+ -(position.inject(0.0) {|s,x| s += x**2 })
31
+ end
32
+
33
+ solver.solve(func, concurrent: true)
34
+ end
35
+
36
+ end
37
+
38
+ context 'sequential' do
39
+
40
+ it 'should solve a non-thread safe function sequentially' do
41
+ # here we create a specially modified version of the function to optimize
42
+ # that raises an error if called concurrently
43
+ mx = Mutex.new
44
+ func = -> position do
45
+ raise "Sequential call check failed" if mx.locked?
46
+ mx.synchronize do
47
+ sleep 0.005
48
+ -(position.inject(0.0) {|s,x| s += x**2 })
49
+ end
50
+ end
51
+
52
+ solver.solve(func)
53
+ end
54
+
17
55
  end
18
56
 
19
57
  end