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,44 @@
|
|
1
|
+
require 'mhl/rechenberg_controller'
|
2
|
+
|
3
|
+
module MHL
|
4
|
+
class HyperMutationPlusRechenbergController
|
5
|
+
DEFAULT_HM_GENERATIONS = 10
|
6
|
+
|
7
|
+
def initialize(params={})
|
8
|
+
@opts = { keep_for: DEFAULT_HM_GENERATIONS }.merge!(params)
|
9
|
+
@rc = RechenbergController.new
|
10
|
+
@gen = @gens_from_last_reset = 1
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(solver, best)
|
14
|
+
if @pending_reset
|
15
|
+
# set mutation_probability to HM value
|
16
|
+
solver.mutation_probability = @pending_reset
|
17
|
+
|
18
|
+
# reinitialize controller
|
19
|
+
@rc = RechenbergController.new
|
20
|
+
|
21
|
+
# reinitialize counter of generations from last reset
|
22
|
+
@gens_from_last_reset = 0
|
23
|
+
|
24
|
+
# undefine pending_reset
|
25
|
+
# NOTE: not sure if we should we go as far as calling
|
26
|
+
# remove_instance_variable(:@pending_reset) here
|
27
|
+
@pending_reset = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# do nothing for the first @opts[:keep_for] generations
|
31
|
+
if @gens_from_last_reset > @opts[:keep_for]
|
32
|
+
@rc.call(solver, best)
|
33
|
+
end
|
34
|
+
|
35
|
+
# update counters
|
36
|
+
@gen += 1
|
37
|
+
@gens_from_last_reset += 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset_mutation_probability(value)
|
41
|
+
@pending_reset = value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -2,7 +2,7 @@ module MHL
|
|
2
2
|
|
3
3
|
# This class implements a genotype with integer space representation
|
4
4
|
class IntegerVectorGenotypeSpace
|
5
|
-
def initialize(opts)
|
5
|
+
def initialize(opts, logger)
|
6
6
|
@random_func = opts[:random_func]
|
7
7
|
|
8
8
|
@dimensions = opts[:dimensions].to_i
|
@@ -13,9 +13,9 @@ module MHL
|
|
13
13
|
# TODO: enable to choose which recombination function to use
|
14
14
|
case opts[:recombination_type].to_s
|
15
15
|
when /intermediate/i
|
16
|
-
@recombination_func = :
|
16
|
+
@recombination_func = :extended_intermediate_recombination_int
|
17
17
|
when /line/i
|
18
|
-
@recombination_func = :
|
18
|
+
@recombination_func = :extended_line_recombination_int
|
19
19
|
else
|
20
20
|
raise ArgumentError, 'Recombination function must be either line or intermediate!'
|
21
21
|
end
|
@@ -25,7 +25,7 @@ module MHL
|
|
25
25
|
raise ArgumentError, 'Constraints must be provided for every dimension!'
|
26
26
|
end
|
27
27
|
|
28
|
-
@logger =
|
28
|
+
@logger = logger
|
29
29
|
end
|
30
30
|
|
31
31
|
def get_random
|
@@ -45,8 +45,8 @@ module MHL
|
|
45
45
|
def reproduce_from(p1, p2, mutation_rv, recombination_rv)
|
46
46
|
# make copies of p1 and p2
|
47
47
|
# (we're only interested in the :genotype key)
|
48
|
-
c1 = { :
|
49
|
-
c2 = { :
|
48
|
+
c1 = { genotype: p1[:genotype].dup }
|
49
|
+
c2 = { genotype: p2[:genotype].dup }
|
50
50
|
|
51
51
|
# mutation comes first
|
52
52
|
random_delta_mutation(c1[:genotype], mutation_rv)
|
@@ -66,6 +66,8 @@ module MHL
|
|
66
66
|
|
67
67
|
private
|
68
68
|
|
69
|
+
# mutation based on random perturbations of (all) the individual's
|
70
|
+
# chromosomes, according to a geometric distribution [TORTONESI16]
|
69
71
|
def random_delta_mutation(g, mutation_rv)
|
70
72
|
g.each_index do |i|
|
71
73
|
delta = mutation_rv.next
|
@@ -80,7 +82,9 @@ module MHL
|
|
80
82
|
end
|
81
83
|
end
|
82
84
|
|
83
|
-
|
85
|
+
# integer variant of extended intermediate recombination [MUHLENBEIN93]
|
86
|
+
# (see [LUKE15] page 65)
|
87
|
+
def extended_intermediate_recombination_int(g1, g2, recombination_rv)
|
84
88
|
# TODO: disable this check in non-debugging mode
|
85
89
|
raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
|
86
90
|
|
@@ -97,7 +101,9 @@ module MHL
|
|
97
101
|
end
|
98
102
|
end
|
99
103
|
|
100
|
-
|
104
|
+
# integer variant of extended line recombination [MUHLENBEIN93] (see
|
105
|
+
# [LUKE15] page 64)
|
106
|
+
def extended_line_recombination_int(g1, g2, recombination_rv)
|
101
107
|
# TODO: disable this check in non-debugging mode
|
102
108
|
raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
|
103
109
|
|
@@ -29,11 +29,10 @@ module MHL
|
|
29
29
|
@random_position_func = opts[:random_position_func]
|
30
30
|
@random_velocity_func = opts[:random_velocity_func]
|
31
31
|
|
32
|
-
@start_positions
|
32
|
+
@start_positions = opts[:start_positions]
|
33
|
+
@start_velocities = opts[:start_velocities]
|
33
34
|
|
34
|
-
@exit_condition
|
35
|
-
|
36
|
-
@pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
|
35
|
+
@exit_condition = opts[:exit_condition]
|
37
36
|
|
38
37
|
case opts[:logger]
|
39
38
|
when :stdout
|
@@ -47,7 +46,7 @@ module MHL
|
|
47
46
|
@quiet = opts[:quiet]
|
48
47
|
|
49
48
|
if @logger
|
50
|
-
@logger.level = opts[:log_level] or
|
49
|
+
@logger.level = (opts[:log_level] or :warn)
|
51
50
|
end
|
52
51
|
end
|
53
52
|
|
@@ -107,8 +106,9 @@ module MHL
|
|
107
106
|
raise ArgumentError, "Not enough information to initialize particle velocities!"
|
108
107
|
end
|
109
108
|
|
110
|
-
ChargedSwarm.new(@swarm_size, init_pos,
|
111
|
-
|
109
|
+
ChargedSwarm.new(size: @swarm_size, initial_positions: init_pos,
|
110
|
+
initial_velocities: init_vel,
|
111
|
+
constraints: @constraints, logger: @logger)
|
112
112
|
end
|
113
113
|
|
114
114
|
# initialize variables
|
@@ -120,31 +120,43 @@ module MHL
|
|
120
120
|
iter += 1
|
121
121
|
@logger.info "MultiSwarm QPSO - Starting iteration #{iter}" if @logger
|
122
122
|
|
123
|
-
# create latch to control program termination
|
124
|
-
latch = Concurrent::CountDownLatch.new(@num_swarms * @swarm_size)
|
125
|
-
|
126
123
|
# assess height for every particle
|
127
|
-
|
128
|
-
|
129
|
-
|
124
|
+
if params[:concurrent]
|
125
|
+
# the function to optimize is thread safe: call it multiple times in
|
126
|
+
# a concurrent fashion
|
127
|
+
# to this end, we use the high level promise-based construct
|
128
|
+
# recommended by the authors of ruby's (fantastic) concurrent gem
|
129
|
+
promises = swarms.map do |swarm|
|
130
|
+
swarm.map do |particle|
|
131
|
+
Concurrent::Promise.execute do
|
132
|
+
# evaluate target function
|
133
|
+
particle.evaluate(func)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end.flatten!
|
137
|
+
|
138
|
+
# wait for all the spawned threads to finish
|
139
|
+
promises.map(&:wait)
|
140
|
+
else
|
141
|
+
# the function to optimize is not thread safe: call it multiple times
|
142
|
+
# in a sequential fashion
|
143
|
+
swarms.each do |swarm|
|
144
|
+
swarm.each do |particle|
|
130
145
|
# evaluate target function
|
131
146
|
particle.evaluate(func)
|
132
|
-
# update latch
|
133
|
-
latch.count_down
|
134
147
|
end
|
135
148
|
end
|
136
149
|
end
|
137
150
|
|
138
|
-
# wait for all the evaluations to end
|
139
|
-
latch.wait
|
140
|
-
|
141
151
|
# update attractors (the highest particle in each swarm)
|
142
152
|
swarm_attractors = swarms.map {|s| s.update_attractor }
|
143
153
|
|
144
154
|
best_attractor = swarm_attractors.max_by {|x| x[:height] }
|
145
155
|
|
146
156
|
# print results
|
147
|
-
|
157
|
+
if @logger and !@quiet
|
158
|
+
@logger.info "> iter #{iter}, best: #{best_attractor[:position]}, #{best_attractor[:height]}"
|
159
|
+
end
|
148
160
|
|
149
161
|
# calculate overall best
|
150
162
|
if overall_best.nil?
|
@@ -26,8 +26,6 @@ module MHL
|
|
26
26
|
|
27
27
|
@exit_condition = opts[:exit_condition]
|
28
28
|
|
29
|
-
@pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
|
30
|
-
|
31
29
|
case opts[:logger]
|
32
30
|
when :stdout
|
33
31
|
@logger = Logger.new(STDOUT)
|
@@ -40,7 +38,7 @@ module MHL
|
|
40
38
|
@quiet = opts[:quiet]
|
41
39
|
|
42
40
|
if @logger
|
43
|
-
@logger.level = (opts[:log_level] or
|
41
|
+
@logger.level = (opts[:log_level] or :warn)
|
44
42
|
end
|
45
43
|
end
|
46
44
|
|
@@ -100,8 +98,9 @@ module MHL
|
|
100
98
|
end
|
101
99
|
|
102
100
|
# setup particles
|
103
|
-
swarm = PSOSwarm.new(@swarm_size, init_pos,
|
104
|
-
|
101
|
+
swarm = PSOSwarm.new(size: @swarm_size, initial_positions: init_pos,
|
102
|
+
initial_velocities: init_vel,
|
103
|
+
constraints: @constraints, logger: @logger)
|
105
104
|
|
106
105
|
# initialize variables
|
107
106
|
iter = 0
|
@@ -112,27 +111,37 @@ module MHL
|
|
112
111
|
iter += 1
|
113
112
|
@logger.info("PSO - Starting iteration #{iter}") if @logger
|
114
113
|
|
115
|
-
# create latch to control program termination
|
116
|
-
latch = Concurrent::CountDownLatch.new(@swarm_size)
|
117
|
-
|
118
114
|
# assess height for every particle
|
119
|
-
|
120
|
-
|
115
|
+
if params[:concurrent]
|
116
|
+
# the function to optimize is thread safe: call it multiple times in
|
117
|
+
# a concurrent fashion
|
118
|
+
# to this end, we use the high level promise-based construct
|
119
|
+
# recommended by the authors of ruby's (fantastic) concurrent gem
|
120
|
+
promises = swarm.map do |particle|
|
121
|
+
Concurrent::Promise.execute do
|
122
|
+
# evaluate target function
|
123
|
+
particle.evaluate(func)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# wait for all the spawned threads to finish
|
128
|
+
promises.map(&:wait)
|
129
|
+
else
|
130
|
+
# the function to optimize is not thread safe: call it multiple times
|
131
|
+
# in a sequential fashion
|
132
|
+
swarm.each do |particle|
|
121
133
|
# evaluate target function
|
122
134
|
particle.evaluate(func)
|
123
|
-
# update latch
|
124
|
-
latch.count_down
|
125
135
|
end
|
126
136
|
end
|
127
137
|
|
128
|
-
# wait for all the threads to terminate
|
129
|
-
latch.wait
|
130
|
-
|
131
138
|
# get swarm attractor (the highest particle)
|
132
139
|
swarm_attractor = swarm.update_attractor
|
133
140
|
|
134
141
|
# print results
|
135
|
-
|
142
|
+
if @logger and !@quiet
|
143
|
+
@logger.info "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}"
|
144
|
+
end
|
136
145
|
|
137
146
|
# calculate overall best (that plays the role of swarm attractor)
|
138
147
|
if overall_best.nil?
|
data/lib/mhl/pso_swarm.rb
CHANGED
@@ -5,7 +5,8 @@ require 'mhl/particle'
|
|
5
5
|
module MHL
|
6
6
|
class PSOSwarm < GenericSwarmBehavior
|
7
7
|
|
8
|
-
def initialize(size
|
8
|
+
def initialize(size:, initial_positions:, initial_velocities:,
|
9
|
+
c1: nil, c2: nil, chi: nil, constraints: nil, logger: nil)
|
9
10
|
@size = size
|
10
11
|
@particles = Array.new(@size) do |index|
|
11
12
|
Particle.new(initial_positions[index], initial_velocities[index])
|
@@ -14,21 +15,22 @@ module MHL
|
|
14
15
|
@iteration = 1
|
15
16
|
|
16
17
|
# get values for parameters C1 and C2
|
17
|
-
@c1 = (
|
18
|
-
@c2 = (
|
18
|
+
@c1 = (c1 || DEFAULT_C1).to_f
|
19
|
+
@c2 = (c2 || DEFAULT_C2).to_f
|
19
20
|
|
20
21
|
# define procedure to get dynamic value for chi
|
21
|
-
@get_chi = if
|
22
|
-
|
22
|
+
@get_chi = if chi and chi.respond_to? :call
|
23
|
+
chi
|
23
24
|
else
|
24
|
-
->(iter) { (
|
25
|
+
->(iter) { (chi || DEFAULT_CHI).to_f }
|
25
26
|
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
end
|
28
|
+
@constraints = constraints
|
29
|
+
@logger = logger
|
30
30
|
|
31
|
-
@constraints
|
31
|
+
if @constraints and @logger
|
32
|
+
@logger.info "PSOSwarm called w/ constraints: #{@constraints}"
|
33
|
+
end
|
32
34
|
end
|
33
35
|
|
34
36
|
def mutate(params={})
|
data/lib/mhl/qpso_swarm.rb
CHANGED
@@ -5,29 +5,30 @@ require 'mhl/quantum_particle'
|
|
5
5
|
module MHL
|
6
6
|
class QPSOSwarm < GenericSwarmBehavior
|
7
7
|
|
8
|
-
def initialize(size
|
8
|
+
def initialize(size:, initial_positions:, alpha: nil, constraints: nil, logger: nil)
|
9
9
|
@size = size
|
10
10
|
@particles = Array.new(@size) do |index|
|
11
11
|
QuantumParticle.new(initial_positions[index])
|
12
12
|
end
|
13
13
|
|
14
14
|
# find problem dimension
|
15
|
-
@dimension
|
15
|
+
@dimension = initial_positions[0].size
|
16
16
|
|
17
17
|
@iteration = 1
|
18
18
|
|
19
19
|
# define procedure to get dynamic value for alpha
|
20
|
-
@get_alpha = if
|
21
|
-
|
20
|
+
@get_alpha = if alpha and alpha.respond_to? :call
|
21
|
+
alpha
|
22
22
|
else
|
23
|
-
->(it) { (
|
23
|
+
->(it) { (alpha || DEFAULT_ALPHA).to_f }
|
24
24
|
end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
end
|
26
|
+
@constraints = constraints
|
27
|
+
@logger = logger
|
29
28
|
|
30
|
-
@constraints
|
29
|
+
if @constraints and @logger
|
30
|
+
@logger.info "QPSOSwarm called w/ constraints: #{@constraints}"
|
31
|
+
end
|
31
32
|
end
|
32
33
|
|
33
34
|
def mutate
|
@@ -27,8 +27,6 @@ module MHL
|
|
27
27
|
|
28
28
|
@exit_condition = opts[:exit_condition]
|
29
29
|
|
30
|
-
@pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
|
31
|
-
|
32
30
|
case opts[:logger]
|
33
31
|
when :stdout
|
34
32
|
@logger = Logger.new(STDOUT)
|
@@ -41,7 +39,7 @@ module MHL
|
|
41
39
|
@quiet = opts[:quiet]
|
42
40
|
|
43
41
|
if @logger
|
44
|
-
@logger.level = opts[:log_level] or
|
42
|
+
@logger.level = (opts[:log_level] or :warn)
|
45
43
|
end
|
46
44
|
end
|
47
45
|
|
@@ -66,7 +64,7 @@ module MHL
|
|
66
64
|
min = @constraints[:min]
|
67
65
|
max = @constraints[:max]
|
68
66
|
# randomization is independent along each dimension
|
69
|
-
|
67
|
+
min.zip(max).map do |min_i,max_i|
|
70
68
|
min_i + SecureRandom.random_number * (max_i - min_i)
|
71
69
|
end
|
72
70
|
end
|
@@ -74,8 +72,8 @@ module MHL
|
|
74
72
|
raise ArgumentError, "Not enough information to initialize particle positions!"
|
75
73
|
end
|
76
74
|
|
77
|
-
swarm = QPSOSwarm.new(@swarm_size, init_pos,
|
78
|
-
|
75
|
+
swarm = QPSOSwarm.new(size: @swarm_size, initial_positions: init_pos,
|
76
|
+
constraints: @constraints, logger: @logger)
|
79
77
|
|
80
78
|
# initialize variables
|
81
79
|
iter = 0
|
@@ -86,26 +84,36 @@ module MHL
|
|
86
84
|
iter += 1
|
87
85
|
@logger.info "QPSO - Starting iteration #{iter}" if @logger
|
88
86
|
|
89
|
-
|
90
|
-
|
87
|
+
if params[:concurrent]
|
88
|
+
# the function to optimize is thread safe: call it multiple times in
|
89
|
+
# a concurrent fashion
|
90
|
+
# to this end, we use the high level promise-based construct
|
91
|
+
# recommended by the authors of ruby's (fantastic) concurrent gem
|
92
|
+
promises = swarm.map do |particle|
|
93
|
+
Concurrent::Promise.execute do
|
94
|
+
# evaluate target function
|
95
|
+
particle.evaluate(func)
|
96
|
+
end
|
97
|
+
end
|
91
98
|
|
92
|
-
|
93
|
-
|
99
|
+
# wait for all the spawned threads to finish
|
100
|
+
promises.map(&:wait)
|
101
|
+
else
|
102
|
+
# the function to optimize is not thread safe: call it multiple times
|
103
|
+
# in a sequential fashion
|
104
|
+
swarm.each do |particle|
|
94
105
|
# evaluate target function
|
95
106
|
particle.evaluate(func)
|
96
|
-
# update latch
|
97
|
-
latch.count_down
|
98
107
|
end
|
99
108
|
end
|
100
109
|
|
101
|
-
# wait for all the evaluations to end
|
102
|
-
latch.wait
|
103
|
-
|
104
110
|
# get swarm attractor (the highest particle)
|
105
111
|
swarm_attractor = swarm.update_attractor
|
106
112
|
|
107
113
|
# print results
|
108
|
-
|
114
|
+
if @logger and !@quiet
|
115
|
+
@logger.info "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}"
|
116
|
+
end
|
109
117
|
|
110
118
|
# calculate overall best (that plays the role of swarm attractor)
|
111
119
|
if overall_best.nil?
|