mhl 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.projections.json +12 -0
- data/.travis.yml +6 -0
- data/README.md +58 -38
- data/Rakefile +3 -0
- data/TODO +24 -0
- data/extra/process_ga_solver_output.awk +2 -0
- data/extra/process_pso_solver_output.awk +2 -0
- data/lib/mhl/charged_swarm.rb +17 -14
- data/lib/mhl/genetic_algorithm_solver.rb +78 -54
- data/lib/mhl/hm_plus_rechenberg_controller.rb +44 -0
- data/lib/mhl/{integer_genotype_space.rb → integer_vector_genotype_space.rb} +14 -8
- data/lib/mhl/multiswarm_qpso_solver.rb +31 -19
- data/lib/mhl/particle_swarm_optimization_solver.rb +25 -16
- data/lib/mhl/pso_swarm.rb +12 -10
- data/lib/mhl/qpso_swarm.rb +10 -9
- data/lib/mhl/quantum_particle_swarm_optimization_solver.rb +24 -16
- data/lib/mhl/real_vector_genotype_space.rb +142 -0
- data/lib/mhl/rechenberg_controller.rb +49 -0
- data/lib/mhl/version.rb +1 -1
- data/mhl.gemspec +4 -1
- data/test/mhl/genetic_algorithm_solver_test.rb +69 -31
- data/test/mhl/{integer_genotype_space_test.rb → integer_vector_genotype_space_test.rb} +34 -26
- data/test/mhl/multiswarm_qpso_solver_test.rb +48 -10
- data/test/mhl/particle_swarm_optimization_solver_test.rb +47 -9
- data/test/mhl/quantum_particle_swarm_optimization_solver_test.rb +47 -9
- data/test/mhl/real_vector_genotype_space_test.rb +85 -0
- data/test/test_helper.rb +1 -0
- metadata +72 -20
@@ -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
|
data/lib/mhl/version.rb
CHANGED
data/mhl.gemspec
CHANGED
@@ -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
|
-
:
|
8
|
-
:
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
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
|
15
|
-
|
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
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
:
|
27
|
-
:
|
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
|
30
|
-
|
37
|
+
logger: logger,
|
38
|
+
log_level: log_level,
|
31
39
|
)
|
32
40
|
end
|
33
41
|
|
34
|
-
|
35
|
-
|
36
|
-
:
|
37
|
-
:
|
38
|
-
:
|
39
|
-
:
|
40
|
-
:
|
41
|
-
:
|
42
|
-
:
|
43
|
-
:
|
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
|
-
:
|
46
|
-
:logger
|
47
|
-
|
53
|
+
exit_condition: lambda {|generation,best_sample| best_sample[:fitness] == 0},
|
54
|
+
logger: logger,
|
55
|
+
log_level: log_level,
|
48
56
|
)
|
49
|
-
|
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(
|
6
|
-
l.level = ENV['DEBUG'] ?
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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 = { :
|
61
|
-
g2 = { :
|
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(:
|
65
|
-
:probability_of_success
|
66
|
-
ERV::RandomVariable.new(:
|
67
|
-
:min_value
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
:
|
13
|
-
:logger
|
14
|
-
|
20
|
+
exit_condition: lambda {|iteration,best| best[:height].abs < 0.001 },
|
21
|
+
logger: logger,
|
22
|
+
log_level: log_level,
|
15
23
|
)
|
16
|
-
|
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
|