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.
- 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
|