mhl 0.1.0 → 0.2.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 +4 -4
- data/README.md +203 -12
- data/lib/mhl/charged_swarm.rb +31 -19
- data/lib/mhl/generic_particle.rb +0 -28
- data/lib/mhl/generic_swarm.rb +12 -10
- data/lib/mhl/genetic_algorithm_solver.rb +1 -2
- data/lib/mhl/multiswarm_qpso_solver.rb +62 -24
- data/lib/mhl/particle.rb +47 -12
- data/lib/mhl/particle_swarm_optimization_solver.rb +67 -33
- data/lib/mhl/pso_swarm.rb +11 -14
- data/lib/mhl/qpso_swarm.rb +23 -12
- data/lib/mhl/quantum_particle.rb +29 -10
- data/lib/mhl/quantum_particle_swarm_optimization_solver.rb +35 -21
- data/lib/mhl/version.rb +1 -1
- data/mhl.gemspec +2 -3
- data/test/mhl/multiswarm_qpso_solver_test.rb +5 -4
- data/test/mhl/particle_swarm_optimization_solver_test.rb +5 -4
- data/test/mhl/quantum_particle_swarm_optimization_solver_test.rb +5 -3
- metadata +18 -32
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'concurrent'
|
2
2
|
require 'erv'
|
3
|
-
require 'facter'
|
4
3
|
require 'logger'
|
5
4
|
|
6
5
|
require 'mhl/bitstring_genotype_space'
|
@@ -57,7 +56,7 @@ module MHL
|
|
57
56
|
|
58
57
|
@controller = opts[:controller]
|
59
58
|
|
60
|
-
@pool = Concurrent::FixedThreadPool.new(
|
59
|
+
@pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
|
61
60
|
|
62
61
|
case opts[:logger]
|
63
62
|
when :stdout
|
@@ -1,7 +1,5 @@
|
|
1
1
|
require 'concurrent'
|
2
|
-
require 'facter'
|
3
2
|
require 'logger'
|
4
|
-
require 'matrix'
|
5
3
|
|
6
4
|
require 'mhl/charged_swarm'
|
7
5
|
|
@@ -16,24 +14,26 @@ module MHL
|
|
16
14
|
# 489-500, Springer, 2004. DOI: 10.1007/978-3-540-24653-4_50
|
17
15
|
class MultiSwarmQPSOSolver
|
18
16
|
|
17
|
+
DEFAULT_SWARM_SIZE = 20
|
18
|
+
|
19
19
|
def initialize(opts={})
|
20
|
-
@swarm_size = opts[:swarm_size].to_i
|
21
|
-
unless @swarm_size
|
22
|
-
raise ArgumentError, 'Swarm size is a required parameter!'
|
23
|
-
end
|
20
|
+
@swarm_size = opts[:swarm_size].try(:to_i) || DEFAULT_SWARM_SIZE
|
24
21
|
|
25
22
|
@num_swarms = opts[:num_swarms].to_i
|
26
23
|
unless @num_swarms
|
27
24
|
raise ArgumentError, 'Number of swarms is a required parameter!'
|
28
25
|
end
|
29
26
|
|
27
|
+
@constraints = opts[:constraints]
|
28
|
+
|
30
29
|
@random_position_func = opts[:random_position_func]
|
31
30
|
@random_velocity_func = opts[:random_velocity_func]
|
32
31
|
|
33
32
|
@start_positions = opts[:start_positions]
|
33
|
+
|
34
34
|
@exit_condition = opts[:exit_condition]
|
35
35
|
|
36
|
-
@pool = Concurrent::FixedThreadPool.new(
|
36
|
+
@pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
|
37
37
|
|
38
38
|
case opts[:logger]
|
39
39
|
when :stdout
|
@@ -58,29 +58,67 @@ module MHL
|
|
58
58
|
# parameters) and returns the phenotype (that is, the function result)
|
59
59
|
def solve(func, params={})
|
60
60
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
61
|
+
swarms = Array.new(@num_swarms) do |index|
|
62
|
+
# initialize particle positions
|
63
|
+
init_pos = if @start_positions
|
64
|
+
# start positions have the highest priority
|
65
|
+
@start_positions[index * @swarm_size, @swarm_size]
|
66
|
+
elsif @random_position_func
|
67
|
+
# random_position_func has the second highest priority
|
68
|
+
Array.new(@swarm_size) { @random_position_func.call }
|
69
|
+
elsif @constraints
|
70
|
+
# constraints were given, so we use them to initialize particle
|
71
|
+
# positions. to this end, we adopt the SPSO 2006-2011 random position
|
72
|
+
# initialization algorithm [CLERC12].
|
73
|
+
Array.new(@swarm_size) do
|
74
|
+
min = @constraints[:min]
|
75
|
+
max = @constraints[:max]
|
76
|
+
# randomization is independent along each dimension
|
77
|
+
min.zip(max).map do |min_i,max_i|
|
78
|
+
min_i + SecureRandom.random_number * (max_i - min_i)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
else
|
82
|
+
raise ArgumentError, "Not enough information to initialize particle positions!"
|
83
|
+
end
|
84
|
+
|
85
|
+
# initialize particle velocities
|
86
|
+
init_vel = if @start_velocities
|
87
|
+
# start velocities have the highest priority
|
88
|
+
@start_velocities[index * @swarm_size / 2, @swarm_size / 2]
|
89
|
+
elsif @random_velocity_func
|
90
|
+
# random_velocity_func has the second highest priority
|
91
|
+
Array.new(@swarm_size / 2) { @random_velocity_func.call }
|
92
|
+
elsif @constraints
|
93
|
+
# constraints were given, so we use them to initialize particle
|
94
|
+
# velocities. to this end, we adopt the SPSO 2011 random velocity
|
95
|
+
# initialization algorithm [CLERC12].
|
96
|
+
init_pos.map do |p|
|
97
|
+
min = @constraints[:min]
|
98
|
+
max = @constraints[:max]
|
99
|
+
# randomization is independent along each dimension
|
100
|
+
p.zip(min,max).map do |p_i,min_i,max_i|
|
101
|
+
min_vel = min_i - p_i
|
102
|
+
max_vel = max_i - p_i
|
103
|
+
min_vel + SecureRandom.random_number * (max_vel - min_vel)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
else
|
107
|
+
raise ArgumentError, "Not enough information to initialize particle velocities!"
|
68
108
|
end
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
# { position: Vector[*pos] }
|
73
|
-
# end
|
109
|
+
|
110
|
+
ChargedSwarm.new(@swarm_size, init_pos, init_vel,
|
111
|
+
params.merge(constraints: @constraints))
|
74
112
|
end
|
75
113
|
|
76
114
|
# initialize variables
|
77
|
-
|
115
|
+
iter = 0
|
78
116
|
overall_best = nil
|
79
117
|
|
80
118
|
# default behavior is to loop forever
|
81
119
|
begin
|
82
|
-
|
83
|
-
@logger.info "
|
120
|
+
iter += 1
|
121
|
+
@logger.info "MultiSwarm QPSO - Starting iteration #{iter}" if @logger
|
84
122
|
|
85
123
|
# create latch to control program termination
|
86
124
|
latch = Concurrent::CountDownLatch.new(@num_swarms * @swarm_size)
|
@@ -106,7 +144,7 @@ module MHL
|
|
106
144
|
best_attractor = swarm_attractors.max_by {|x| x[:height] }
|
107
145
|
|
108
146
|
# print results
|
109
|
-
puts ">
|
147
|
+
puts "> iter #{iter}, best: #{best_attractor[:position]}, #{best_attractor[:height]}" unless @quiet
|
110
148
|
|
111
149
|
# calculate overall best
|
112
150
|
if overall_best.nil?
|
@@ -130,7 +168,7 @@ module MHL
|
|
130
168
|
# mutate swarms
|
131
169
|
swarms.each {|s| s.mutate }
|
132
170
|
|
133
|
-
end while @exit_condition.nil? or !@exit_condition.call(
|
171
|
+
end while @exit_condition.nil? or !@exit_condition.call(iter, overall_best)
|
134
172
|
|
135
173
|
overall_best
|
136
174
|
end
|
data/lib/mhl/particle.rb
CHANGED
@@ -10,23 +10,58 @@ module MHL
|
|
10
10
|
end
|
11
11
|
|
12
12
|
# move particle and update attractor
|
13
|
-
def move(
|
13
|
+
def move(chi, c1, c2, swarm_attractor)
|
14
14
|
raise 'Particle attractor is nil!' if @attractor.nil?
|
15
|
-
|
15
|
+
|
16
|
+
# update particle velocity and position according to the Constrained PSO
|
17
|
+
# variant of the Particle Swarm Optimization algorithm:
|
18
|
+
#
|
19
|
+
# V_{i,j}(t+1) = \chi [ V_{i,j}(t) + \\
|
20
|
+
# C_1 * r_{i,j}(t) * (P_{i,j}(t) - X_{i,j}(t)) + \\
|
21
|
+
# C_2 * R_{i,j}(t) * (G_j(t) - X_{i,j}(t)) ] \\
|
22
|
+
# X_{i,j}(t+1) = X_{i,j}(t) + V_{i,j}(t+1)
|
23
|
+
#
|
24
|
+
# see equation 4.30 of [SUN11].
|
16
25
|
|
17
26
|
# update velocity
|
18
|
-
@velocity =
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
+
@velocity = @velocity.zip(@position, @attractor[:position], swarm_attractor[:position]).map do |v_j,x_j,p_j,g_j|
|
28
|
+
# everything is damped by inertia weight chi
|
29
|
+
chi *
|
30
|
+
#previous velocity
|
31
|
+
(v_j +
|
32
|
+
# "memory" component (linear attraction towards the best position
|
33
|
+
# that this particle encountered so far)
|
34
|
+
c1 * SecureRandom.random_number * (p_j - x_j) +
|
35
|
+
# "social" component (linear attraction towards the best position
|
36
|
+
# that the entire swarm encountered so far)
|
37
|
+
c2 * SecureRandom.random_number * (g_j - x_j))
|
38
|
+
end
|
27
39
|
|
28
40
|
# update position
|
29
|
-
@position = @position
|
41
|
+
@position = @position.zip(@velocity).map do |x_j,v_j|
|
42
|
+
x_j + v_j
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# implement confinement à la SPSO 2011. for more information, see equations
|
47
|
+
# 3.14 and 3.15 of [CLERC12].
|
48
|
+
def remain_within(constraints)
|
49
|
+
@position = @position.map.with_index do |x_j,j|
|
50
|
+
d_max = constraints[:max][j]
|
51
|
+
d_min = constraints[:min][j]
|
52
|
+
if x_j > d_max
|
53
|
+
# puts "resetting #{j}-th position component #{x_j} to #{d_max}"
|
54
|
+
x_j = d_max
|
55
|
+
# puts "resetting #{j}-th velocity component #{@velocity[j]} to #{-0.5 * @velocity[j]}"
|
56
|
+
@velocity[j] = -0.5 * @velocity[j]
|
57
|
+
elsif x_j < d_min
|
58
|
+
# puts "resetting #{j}-th position component #{x_j} to #{d_min}"
|
59
|
+
x_j = d_min
|
60
|
+
# puts "resetting #{j}-th velocity component #{@velocity[j]} to #{-0.5 * @velocity[j]}"
|
61
|
+
@velocity[j] = -0.5 * @velocity[j]
|
62
|
+
end
|
63
|
+
x_j
|
64
|
+
end
|
30
65
|
end
|
31
66
|
|
32
67
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'concurrent'
|
2
|
-
require 'facter'
|
3
2
|
require 'logger'
|
4
3
|
|
5
4
|
require 'mhl/pso_swarm'
|
@@ -7,28 +6,27 @@ require 'mhl/pso_swarm'
|
|
7
6
|
|
8
7
|
module MHL
|
9
8
|
|
10
|
-
# This solver implements the
|
11
|
-
#
|
12
|
-
# For more information, refer to equation 4 of:
|
13
|
-
# [REZAEEJORDEHI13] A. Rezaee Jordehi & J. Jasni (2013) Parameter selection
|
14
|
-
# in particle swarm optimisation: a survey, Journal of Experimental &
|
15
|
-
# Theoretical Artificial Intelligence, 25:4, pp. 527-542, DOI:
|
16
|
-
# 10.1080/0952813X.2013.782348
|
9
|
+
# This solver implements the "canonical" variant of PSO called Constrained
|
10
|
+
# PSO. For more information, refer to equation 4.30 of [SUN11].
|
17
11
|
class ParticleSwarmOptimizationSolver
|
18
12
|
|
13
|
+
# This is the default swarm size recommended by SPSO 2011 [CLERC12].
|
14
|
+
DEFAULT_SWARM_SIZE = 40
|
15
|
+
|
19
16
|
def initialize(opts={})
|
20
|
-
@swarm_size = opts[:swarm_size].to_i
|
21
|
-
|
22
|
-
|
23
|
-
end
|
17
|
+
@swarm_size = opts[:swarm_size].try(:to_i) || DEFAULT_SWARM_SIZE
|
18
|
+
|
19
|
+
@constraints = opts[:constraints]
|
24
20
|
|
25
21
|
@random_position_func = opts[:random_position_func]
|
26
22
|
@random_velocity_func = opts[:random_velocity_func]
|
27
23
|
|
28
|
-
@start_positions
|
24
|
+
@start_positions = opts[:start_positions]
|
25
|
+
@start_velocities = opts[:start_velocities]
|
26
|
+
|
29
27
|
@exit_condition = opts[:exit_condition]
|
30
28
|
|
31
|
-
@pool = Concurrent::FixedThreadPool.new(
|
29
|
+
@pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
|
32
30
|
|
33
31
|
case opts[:logger]
|
34
32
|
when :stdout
|
@@ -52,31 +50,67 @@ module MHL
|
|
52
50
|
# object) that accepts the genotype as argument (that is, the set of
|
53
51
|
# parameters) and returns the phenotype (that is, the function result)
|
54
52
|
def solve(func, params={})
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
|
54
|
+
# initialize particle positions
|
55
|
+
init_pos = if @start_positions
|
56
|
+
# start positions have the highest priority
|
57
|
+
@start_positions
|
58
|
+
elsif @random_position_func
|
59
|
+
# random_position_func has the second highest priority
|
60
|
+
Array.new(@swarm_size) { @random_position_func.call }
|
61
|
+
elsif @constraints
|
62
|
+
# constraints were given, so we use them to initialize particle
|
63
|
+
# positions. to this end, we adopt the SPSO 2006-2011 random position
|
64
|
+
# initialization algorithm [CLERC12].
|
65
|
+
Array.new(@swarm_size) do
|
66
|
+
min = @constraints[:min]
|
67
|
+
max = @constraints[:max]
|
68
|
+
# randomization is independent along each dimension
|
69
|
+
min.zip(max).map do |min_i,max_i|
|
70
|
+
min_i + SecureRandom.random_number * (max_i - min_i)
|
71
|
+
end
|
72
|
+
end
|
61
73
|
else
|
62
|
-
|
63
|
-
swarm = PSOSwarm.new(@swarm_size,
|
64
|
-
@start_positions.map {|x| Vector[*x] },
|
65
|
-
Array.new(@swarm_size) { Vector[*@random_velocity_func.call] },
|
66
|
-
params)
|
67
|
-
# particles = @start_positions.each_slice(2).map do |pos,vel|
|
68
|
-
# { position: Vector[*pos], velocity: Vector[*vel] }
|
69
|
-
# end
|
74
|
+
raise ArgumentError, "Not enough information to initialize particle positions!"
|
70
75
|
end
|
71
76
|
|
77
|
+
# initialize particle velocities
|
78
|
+
init_vel = if @start_velocities
|
79
|
+
# start velocities have the highest priority
|
80
|
+
@start_velocities
|
81
|
+
elsif @random_velocity_func
|
82
|
+
# random_velocity_func has the second highest priority
|
83
|
+
Array.new(@swarm_size) { @random_velocity_func.call }
|
84
|
+
elsif @constraints
|
85
|
+
# constraints were given, so we use them to initialize particle
|
86
|
+
# velocities. to this end, we adopt the SPSO 2011 random velocity
|
87
|
+
# initialization algorithm [CLERC12].
|
88
|
+
init_pos.map do |p|
|
89
|
+
min = @constraints[:min]
|
90
|
+
max = @constraints[:max]
|
91
|
+
# randomization is independent along each dimension
|
92
|
+
p.zip(min,max).map do |p_i,min_i,max_i|
|
93
|
+
min_vel = min_i - p_i
|
94
|
+
max_vel = max_i - p_i
|
95
|
+
min_vel + SecureRandom.random_number * (max_vel - min_vel)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
else
|
99
|
+
raise ArgumentError, "Not enough information to initialize particle velocities!"
|
100
|
+
end
|
101
|
+
|
102
|
+
# setup particles
|
103
|
+
swarm = PSOSwarm.new(@swarm_size, init_pos, init_vel,
|
104
|
+
params.merge(constraints: @constraints))
|
105
|
+
|
72
106
|
# initialize variables
|
73
|
-
|
107
|
+
iter = 0
|
74
108
|
overall_best = nil
|
75
109
|
|
76
110
|
# default behavior is to loop forever
|
77
111
|
begin
|
78
|
-
|
79
|
-
@logger.info("PSO - Starting
|
112
|
+
iter += 1
|
113
|
+
@logger.info("PSO - Starting iteration #{iter}") if @logger
|
80
114
|
|
81
115
|
# create latch to control program termination
|
82
116
|
latch = Concurrent::CountDownLatch.new(@swarm_size)
|
@@ -98,7 +132,7 @@ module MHL
|
|
98
132
|
swarm_attractor = swarm.update_attractor
|
99
133
|
|
100
134
|
# print results
|
101
|
-
puts ">
|
135
|
+
puts "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
|
102
136
|
|
103
137
|
# calculate overall best (that plays the role of swarm attractor)
|
104
138
|
if overall_best.nil?
|
@@ -110,7 +144,7 @@ module MHL
|
|
110
144
|
# mutate swarm
|
111
145
|
swarm.mutate
|
112
146
|
|
113
|
-
end while @exit_condition.nil? or !@exit_condition.call(
|
147
|
+
end while @exit_condition.nil? or !@exit_condition.call(iter, overall_best)
|
114
148
|
|
115
149
|
overall_best
|
116
150
|
end
|
data/lib/mhl/pso_swarm.rb
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
require 'matrix'
|
2
|
-
require 'securerandom'
|
3
|
-
|
4
1
|
require 'mhl/generic_swarm'
|
5
2
|
require 'mhl/particle'
|
6
3
|
|
@@ -14,39 +11,39 @@ module MHL
|
|
14
11
|
Particle.new(initial_positions[index], initial_velocities[index])
|
15
12
|
end
|
16
13
|
|
17
|
-
@
|
14
|
+
@iteration = 1
|
18
15
|
|
19
16
|
# get values for parameters C1 and C2
|
20
17
|
@c1 = (params[:c1] || DEFAULT_C1).to_f
|
21
18
|
@c2 = (params[:c1] || DEFAULT_C2).to_f
|
22
19
|
|
23
|
-
# define procedure to get dynamic value for
|
24
|
-
@
|
25
|
-
params[:
|
20
|
+
# define procedure to get dynamic value for chi
|
21
|
+
@get_chi = if params.has_key? :chi and params[:chi].respond_to? :call
|
22
|
+
params[:chi]
|
26
23
|
else
|
27
|
-
->(
|
24
|
+
->(iter) { (params[:chi] || DEFAULT_CHI).to_f }
|
28
25
|
end
|
29
26
|
|
30
27
|
if params.has_key? :constraints
|
31
|
-
puts "PSOSwarm called w/ :
|
28
|
+
puts "PSOSwarm called w/ constraints: #{params[:constraints]}"
|
32
29
|
end
|
33
30
|
|
34
31
|
@constraints = params[:constraints]
|
35
32
|
end
|
36
33
|
|
37
34
|
def mutate(params={})
|
38
|
-
# get
|
39
|
-
|
35
|
+
# get chi parameter
|
36
|
+
chi = @get_chi.call(@iteration)
|
40
37
|
|
41
38
|
# move particles
|
42
|
-
@particles.each_with_index do |p,i|
|
43
|
-
p.move(
|
39
|
+
@particles.each_with_index do |p,i|
|
40
|
+
p.move(chi, @c1, @c2, @swarm_attractor)
|
44
41
|
if @constraints
|
45
42
|
p.remain_within(@constraints)
|
46
43
|
end
|
47
44
|
end
|
48
45
|
|
49
|
-
@
|
46
|
+
@iteration += 1
|
50
47
|
end
|
51
48
|
end
|
52
49
|
end
|
data/lib/mhl/qpso_swarm.rb
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
require 'matrix'
|
2
|
-
require 'securerandom'
|
3
|
-
|
4
1
|
require 'mhl/generic_swarm'
|
5
2
|
require 'mhl/quantum_particle'
|
6
3
|
|
@@ -17,28 +14,42 @@ module MHL
|
|
17
14
|
# find problem dimension
|
18
15
|
@dimension = initial_positions[0].size
|
19
16
|
|
20
|
-
@
|
17
|
+
@iteration = 1
|
21
18
|
|
22
19
|
# define procedure to get dynamic value for alpha
|
23
20
|
@get_alpha = if params.has_key? :alpha and params[:alpha].respond_to? :call
|
24
21
|
params[:alpha]
|
25
22
|
else
|
26
|
-
->(
|
23
|
+
->(it) { (params[:alpha] || DEFAULT_ALPHA).to_f }
|
24
|
+
end
|
25
|
+
|
26
|
+
if params.has_key? :constraints
|
27
|
+
puts "QPSOSwarm called w/ constraints: #{params[:constraints]}"
|
27
28
|
end
|
29
|
+
|
30
|
+
@constraints = params[:constraints]
|
28
31
|
end
|
29
32
|
|
30
33
|
def mutate
|
31
34
|
# get alpha parameter
|
32
|
-
alpha = @get_alpha.call(@
|
35
|
+
alpha = @get_alpha.call(@iteration)
|
33
36
|
|
34
|
-
# this calculates the C_n parameter (
|
35
|
-
#
|
36
|
-
|
37
|
-
c_n =
|
37
|
+
# this calculates the C_n parameter (the centroid of the set of all the
|
38
|
+
# particle attractors) as defined in equations 4.81 and 4.82 of [SUN11].
|
39
|
+
attractors = @particles.map {|p| p.attractor[:position] }
|
40
|
+
c_n = 0.upto(@dimension-1).map do |j|
|
41
|
+
attractors.inject(0.0) {|s,attr| s += attr[j] } / @size.to_f
|
42
|
+
end
|
38
43
|
|
39
|
-
|
44
|
+
# move particles
|
45
|
+
@particles.each do |p|
|
46
|
+
p.move(alpha, c_n, @swarm_attractor)
|
47
|
+
if @constraints
|
48
|
+
p.remain_within(@constraints)
|
49
|
+
end
|
50
|
+
end
|
40
51
|
|
41
|
-
@
|
52
|
+
@iteration += 1
|
42
53
|
end
|
43
54
|
|
44
55
|
end
|