mhl 0.2.0 → 0.3.0

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