mhl 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +8 -0
- data/lib/mhl.rb +2 -0
- data/lib/mhl/charged_swarm.rb +85 -0
- data/lib/mhl/generic_particle.rb +52 -0
- data/lib/mhl/generic_swarm.rb +41 -0
- data/lib/mhl/genetic_algorithm_solver.rb +64 -10
- data/lib/mhl/integer_genotype_space.rb +39 -11
- data/lib/mhl/multiswarm_qpso_solver.rb +140 -0
- data/lib/mhl/particle.rb +33 -0
- data/lib/mhl/particle_swarm_optimization_solver.rb +67 -69
- data/lib/mhl/pso_swarm.rb +52 -0
- data/lib/mhl/qpso_swarm.rb +45 -0
- data/lib/mhl/quantum_particle.rb +40 -0
- data/lib/mhl/quantum_particle_swarm_optimization_solver.rb +113 -0
- data/lib/mhl/version.rb +1 -1
- data/mhl.gemspec +2 -2
- data/test/mhl/genetic_algorithm_solver_test.rb +52 -0
- data/test/mhl/integer_genotype_space_test.rb +77 -0
- data/test/mhl/multiswarm_qpso_solver_test.rb +18 -0
- data/{spec/mhl/particle_swarm_optimization_solver_spec.rb → test/mhl/particle_swarm_optimization_solver_test.rb} +3 -2
- data/test/mhl/quantum_particle_swarm_optimization_solver_test.rb +16 -0
- data/test/test_helper.rb +6 -0
- metadata +49 -34
- data/spec/mhl/genetic_algorithm_spec.rb +0 -53
- data/spec/spec_helper.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6451520f742d76561c0caf54cde03a7da4643898
|
4
|
+
data.tar.gz: f37fb25a97bb1b718c5ab2e7078f6853574b3749
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa0d5beb542b0889e042486421d8d878b427042c046d8c72b9cea1c4913d9bb01a5ec0f9d38bb34c6d48de49723346c2f9430d30a593bf1eb01d9786292270ea
|
7
|
+
data.tar.gz: a848727a22034af0a4a59bd58c3db415af71715e3e9fce35d17305312d4649cbecf93d2ee78eee157e3d5dcfc8b158a6ba7b85901e2236c87ce82d159878b7ad
|
data/Rakefile
CHANGED
data/lib/mhl.rb
CHANGED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
|
3
|
+
require 'mhl/generic_swarm'
|
4
|
+
|
5
|
+
|
6
|
+
module MHL
|
7
|
+
class ChargedSwarm < GenericSwarmBehavior
|
8
|
+
|
9
|
+
# default composition is half charged, i.e., QPSO, and half neutral, i.e.,
|
10
|
+
# traditional PSO (with inertia), swarms
|
11
|
+
DEFAULT_CHARGED_TO_NEUTRAL_RATIO = 1.0
|
12
|
+
|
13
|
+
def initialize(size, initial_positions, initial_velocities, params={})
|
14
|
+
@size = size
|
15
|
+
|
16
|
+
# retrieve ratio between charged (QPSO) and neutral (PSO w/ inertia) particles
|
17
|
+
ratio = (params[:charged_to_neutral_ratio] || DEFAULT_CHARGED_TO_NEUTRAL_RATIO).to_f
|
18
|
+
unless ratio > 0.0
|
19
|
+
raise ArgumentError, 'Parameter :charged_to_neutral_ratio should be a real greater than zero!'
|
20
|
+
end
|
21
|
+
|
22
|
+
num_charged_particles = (@size * ratio).round
|
23
|
+
@num_neutral_particles = @size - num_charged_particles
|
24
|
+
|
25
|
+
# the particles are ordered, with neutral (PSO w/ inertia) particles
|
26
|
+
# first and charged (QPSO) particles later
|
27
|
+
@particles = Array.new(@size) do |index|
|
28
|
+
if index < @num_neutral_particles
|
29
|
+
Particle.new(initial_positions[index], initial_velocities[index])
|
30
|
+
else
|
31
|
+
QuantumParticle.new(initial_positions[index])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# find problem dimension
|
36
|
+
@dimension = initial_positions[0].size
|
37
|
+
|
38
|
+
@generation = 1
|
39
|
+
|
40
|
+
# define procedure to get dynamic value for alpha
|
41
|
+
@get_alpha = if params.has_key? :alpha and params[:alpha].respond_to? :call
|
42
|
+
params[:alpha]
|
43
|
+
else
|
44
|
+
->(gen) { (params[:alpha] || DEFAULT_ALPHA).to_f }
|
45
|
+
end
|
46
|
+
|
47
|
+
# get values for parameters C1 and C2
|
48
|
+
@c1 = (params[:c1] || DEFAULT_C1).to_f
|
49
|
+
@c2 = (params[:c1] || DEFAULT_C2).to_f
|
50
|
+
|
51
|
+
# define procedure to get dynamic value for omega
|
52
|
+
@get_omega = if params.has_key? :omega and params[:omega].respond_to? :call
|
53
|
+
params[:omega]
|
54
|
+
else
|
55
|
+
->(gen) { (params[:omega] || DEFAULT_OMEGA).to_f }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def mutate
|
60
|
+
# get alpha parameter
|
61
|
+
alpha = @get_alpha.call(@generation)
|
62
|
+
|
63
|
+
# get omega parameter
|
64
|
+
omega = @get_omega.call(@generation)
|
65
|
+
|
66
|
+
# this calculates the C_n parameter (basically, the centroid of particle
|
67
|
+
# attractors) as defined in [SUN11], formulae 4.81 and 4.82
|
68
|
+
#
|
69
|
+
# (note: the neutral particles influence the behavior of the charged ones
|
70
|
+
# not only by defining the swarm attractor, but also by forming this centroid)
|
71
|
+
c_n = @particles.inject(Vector[*[0]*@dimension]) {|s,p| s += p.attractor[:position] } / @size.to_f
|
72
|
+
|
73
|
+
@particles.each_with_index do |p,i|
|
74
|
+
# remember: the particles are kept in a PSO-first and QPSO-last order
|
75
|
+
if i < @num_neutral_particles
|
76
|
+
p.move(omega, @c1, @c2, @swarm_attractor)
|
77
|
+
else
|
78
|
+
p.move(alpha, c_n, @swarm_attractor)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
@generation += 1
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module MHL
|
2
|
+
|
3
|
+
class GenericParticle
|
4
|
+
|
5
|
+
attr_reader :attractor
|
6
|
+
|
7
|
+
def initialize(initial_position)
|
8
|
+
@position = initial_position
|
9
|
+
@attractor = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(func)
|
13
|
+
# calculate particle height
|
14
|
+
@height = func.call(@position)
|
15
|
+
|
16
|
+
# update particle attractor (if needed)
|
17
|
+
if @attractor.nil? or @height > @attractor[:height]
|
18
|
+
@attractor = { height: @height, position: @position }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def remain_within(constraints)
|
23
|
+
new_pos = @position.map.with_index do |x,i|
|
24
|
+
puts "resetting #{x} within #{constraints[:min][i]} and #{constraints[:max][i]}"
|
25
|
+
d_max = constraints[:max][i]
|
26
|
+
d_min = constraints[:min][i]
|
27
|
+
d_size = d_max - d_min
|
28
|
+
if x > d_max
|
29
|
+
while x > d_max + d_size
|
30
|
+
x -= d_size
|
31
|
+
end
|
32
|
+
if x > d_max
|
33
|
+
x = 2 * d_max - x
|
34
|
+
end
|
35
|
+
elsif x < d_min
|
36
|
+
while x < d_min - d_size
|
37
|
+
x += d_size
|
38
|
+
end
|
39
|
+
if x < d_min
|
40
|
+
x = 2 * d_min - x
|
41
|
+
end
|
42
|
+
end
|
43
|
+
puts "now x is #{x}"
|
44
|
+
x
|
45
|
+
end
|
46
|
+
puts "new_pos: #{new_pos}"
|
47
|
+
@position = new_pos # Vector[new_pos]
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module MHL
|
4
|
+
class GenericSwarmBehavior
|
5
|
+
|
6
|
+
# The following values were taken from [BLACKWELLBRANKE04] Tim Blackwell,
|
7
|
+
# Jürgen Branke, "Multi-swarm Optimization in Dynamic Environments",
|
8
|
+
# Applications of Evolutionary Computing, pp. 489-500, Springer, 2004.
|
9
|
+
# DOI: 10.1007/978-3-540-24653-4_50
|
10
|
+
# C_1 is the cognitive acceleration coefficient
|
11
|
+
DEFAULT_C1 = 2.05
|
12
|
+
# C_2 is the social acceleration coefficient
|
13
|
+
DEFAULT_C2 = 2.05
|
14
|
+
PHI = DEFAULT_C1 + DEFAULT_C2
|
15
|
+
# \omega is the inertia weight
|
16
|
+
DEFAULT_OMEGA = 2.0 / (2 - PHI - Math.sqrt(PHI ** 2 - 4.0 * PHI)).abs
|
17
|
+
|
18
|
+
# \alpha is the inertia weight
|
19
|
+
# According to [SUN11], this looks like a sensible default parameter
|
20
|
+
DEFAULT_ALPHA = 0.75
|
21
|
+
|
22
|
+
extend Forwardable
|
23
|
+
def_delegators :@particles, :each
|
24
|
+
|
25
|
+
include Enumerable
|
26
|
+
|
27
|
+
def update_attractor
|
28
|
+
# get the particle attractors
|
29
|
+
particle_attractors = @particles.map { |p| p.attractor }
|
30
|
+
|
31
|
+
# update swarm attractor (if needed)
|
32
|
+
if @swarm_attractor.nil?
|
33
|
+
@swarm_attractor = particle_attractors.max_by {|p| p[:height] }
|
34
|
+
else
|
35
|
+
@swarm_attractor = [ @swarm_attractor, *particle_attractors ].max_by {|p| p[:height] }
|
36
|
+
end
|
37
|
+
|
38
|
+
@swarm_attractor
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'concurrent'
|
2
2
|
require 'erv'
|
3
|
+
require 'facter'
|
4
|
+
require 'logger'
|
3
5
|
|
4
6
|
require 'mhl/bitstring_genotype_space'
|
5
7
|
require 'mhl/integer_genotype_space'
|
@@ -8,6 +10,9 @@ require 'mhl/integer_genotype_space'
|
|
8
10
|
module MHL
|
9
11
|
|
10
12
|
class GeneticAlgorithmSolver
|
13
|
+
# mutation_probability is the parameter that controls the intensity of mutation
|
14
|
+
attr_reader :mutation_probability
|
15
|
+
|
11
16
|
def initialize(opts)
|
12
17
|
@population_size = opts[:population_size].to_i
|
13
18
|
unless @population_size and @population_size.even?
|
@@ -20,10 +25,10 @@ module MHL
|
|
20
25
|
@genotype_space = IntegerVectorGenotypeSpace.new(opts[:genotype_space_conf])
|
21
26
|
|
22
27
|
begin
|
23
|
-
|
28
|
+
@mutation_probability = opts[:mutation_probability].to_f
|
24
29
|
@mutation_rv = \
|
25
30
|
ERV::RandomVariable.new(:distribution => :geometric,
|
26
|
-
:probability_of_success =>
|
31
|
+
:probability_of_success => @mutation_probability)
|
27
32
|
rescue
|
28
33
|
raise ArgumentError, 'Mutation probability configuration is wrong.'
|
29
34
|
end
|
@@ -49,8 +54,36 @@ module MHL
|
|
49
54
|
|
50
55
|
@exit_condition = opts[:exit_condition]
|
51
56
|
@start_population = opts[:genotype_space_conf][:start_population]
|
57
|
+
|
58
|
+
@controller = opts[:controller]
|
59
|
+
|
60
|
+
@pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
|
61
|
+
|
62
|
+
case opts[:logger]
|
63
|
+
when :stdout
|
64
|
+
@logger = Logger.new(STDOUT)
|
65
|
+
when :stderr
|
66
|
+
@logger = Logger.new(STDERR)
|
67
|
+
else
|
68
|
+
@logger = opts[:logger]
|
69
|
+
end
|
70
|
+
|
71
|
+
@quiet = opts[:quiet]
|
72
|
+
|
73
|
+
if @logger
|
74
|
+
@logger.level = (opts[:log_level] or Logger::WARN)
|
75
|
+
end
|
52
76
|
end
|
53
77
|
|
78
|
+
def mutation_probability=(new_mp)
|
79
|
+
unless new_mp > 0.0 and new_mp < 1.0
|
80
|
+
raise ArgumentError, 'Mutation probability needs to be > 0 and < 1'
|
81
|
+
end
|
82
|
+
@mutation_probability = new_mp
|
83
|
+
@mutation_rv = \
|
84
|
+
ERV::RandomVariable.new(:distribution => :geometric,
|
85
|
+
:probability_of_success => @mutation_probability)
|
86
|
+
end
|
54
87
|
|
55
88
|
# This is the method that solves the optimization problem
|
56
89
|
#
|
@@ -74,24 +107,42 @@ module MHL
|
|
74
107
|
gen = 0
|
75
108
|
overall_best = nil
|
76
109
|
|
110
|
+
population_mutex = Mutex.new
|
111
|
+
|
77
112
|
# default behavior is to loop forever
|
78
113
|
begin
|
79
114
|
gen += 1
|
80
|
-
|
115
|
+
@logger.info("GA - Starting generation #{gen}") if @logger
|
116
|
+
|
117
|
+
# create latch to control program termination
|
118
|
+
latch = Concurrent::CountDownLatch.new(@population_size)
|
81
119
|
|
82
120
|
# assess fitness for every member of the population
|
83
121
|
population.each do |s|
|
84
|
-
|
122
|
+
@pool.post do
|
123
|
+
# do we need to syncronize this call through population_mutex?
|
124
|
+
# probably not.
|
125
|
+
ret = func.call(s[:genotype])
|
126
|
+
|
127
|
+
# protect write access to population struct using mutex
|
128
|
+
population_mutex.synchronize do
|
129
|
+
s[:fitness] = ret
|
130
|
+
end
|
131
|
+
|
132
|
+
# update latch
|
133
|
+
latch.count_down
|
134
|
+
end
|
85
135
|
end
|
86
136
|
|
87
|
-
# wait for all the
|
88
|
-
|
89
|
-
s[:fitness] = s[:task].value
|
90
|
-
end
|
137
|
+
# wait for all the threads to terminate
|
138
|
+
latch.wait
|
91
139
|
|
92
140
|
# find fittest member
|
93
141
|
population_best = population.max_by {|x| x[:fitness] }
|
94
142
|
|
143
|
+
# print results
|
144
|
+
puts "> gen #{gen}, best: #{population_best[:genotype]}, #{population_best[:fitness]}" unless @quiet
|
145
|
+
|
95
146
|
# calculate overall best
|
96
147
|
if overall_best.nil?
|
97
148
|
overall_best = population_best
|
@@ -99,8 +150,8 @@ module MHL
|
|
99
150
|
overall_best = [ overall_best, population_best ].max_by {|x| x[:fitness] }
|
100
151
|
end
|
101
152
|
|
102
|
-
#
|
103
|
-
|
153
|
+
# execute controller
|
154
|
+
@controller.call(self, overall_best) if @controller
|
104
155
|
|
105
156
|
# selection by binary tournament
|
106
157
|
children = new_generation(population)
|
@@ -108,6 +159,9 @@ module MHL
|
|
108
159
|
# update population and generation number
|
109
160
|
population = children
|
110
161
|
end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
|
162
|
+
|
163
|
+
# return best sample
|
164
|
+
overall_best
|
111
165
|
end
|
112
166
|
|
113
167
|
|
@@ -19,13 +19,24 @@ module MHL
|
|
19
19
|
else
|
20
20
|
raise ArgumentError, 'Recombination function must be either line or intermediate!'
|
21
21
|
end
|
22
|
+
|
23
|
+
@constraints = opts[:constraints]
|
24
|
+
if @constraints and @constraints.size != @dimensions
|
25
|
+
raise ArgumentError, 'Constraints must be provided for every dimension!'
|
26
|
+
end
|
27
|
+
|
28
|
+
@logger = opts[:logger]
|
22
29
|
end
|
23
30
|
|
24
31
|
def get_random
|
25
32
|
if @random_func
|
26
33
|
@random_func.call
|
27
34
|
else
|
28
|
-
|
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
|
29
40
|
end
|
30
41
|
end
|
31
42
|
|
@@ -38,22 +49,25 @@ module MHL
|
|
38
49
|
c2 = { :genotype => p2[:genotype].dup }
|
39
50
|
|
40
51
|
# mutation comes first
|
41
|
-
|
42
|
-
|
52
|
+
random_delta_mutation(c1[:genotype], mutation_rv)
|
53
|
+
random_delta_mutation(c2[:genotype], mutation_rv)
|
43
54
|
|
44
55
|
# and then recombination
|
45
56
|
send(@recombination_func, c1[:genotype], c2[:genotype], recombination_rv)
|
46
57
|
|
58
|
+
if @constraints
|
59
|
+
repair_chromosome(c1[:genotype])
|
60
|
+
repair_chromosome(c2[:genotype])
|
61
|
+
end
|
62
|
+
|
47
63
|
return c1, c2
|
48
64
|
end
|
49
65
|
|
50
66
|
|
51
67
|
private
|
52
68
|
|
53
|
-
def
|
69
|
+
def random_delta_mutation(g, mutation_rv)
|
54
70
|
g.each_index do |i|
|
55
|
-
# being sampled from a geometric distribution, delta will always
|
56
|
-
# be a non-negative integer (that is, 0 or greater)
|
57
71
|
delta = mutation_rv.next
|
58
72
|
|
59
73
|
if rand() >= 0.5
|
@@ -77,7 +91,7 @@ module MHL
|
|
77
91
|
beta = recombination_rv.next
|
78
92
|
t = (alpha * g1[i] + (1.0 - alpha) * g2[i] + 0.5).floor
|
79
93
|
s = ( beta * g2[i] + (1.0 - beta) * g1[i] + 0.5).floor
|
80
|
-
end
|
94
|
+
end
|
81
95
|
g1[i] = t
|
82
96
|
g2[i] = s
|
83
97
|
end
|
@@ -94,10 +108,24 @@ module MHL
|
|
94
108
|
g1.each_index do |i|
|
95
109
|
t = (alpha * g1[i] + (1.0 - alpha) * g2[i] + 0.5).floor
|
96
110
|
s = ( beta * g2[i] + (1.0 - beta) * g1[i] + 0.5).floor
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
111
|
+
g1[i] = t
|
112
|
+
g2[i] = s
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def repair_chromosome(g)
|
117
|
+
g.each_index do |i|
|
118
|
+
if g[i] < @constraints[i][:from]
|
119
|
+
range = "[#{@constraints[i][:from]},#{@constraints[i][:to]}]"
|
120
|
+
@logger.debug "repairing g[#{i}] #{g[i]} to fit within #{range}" if @logger
|
121
|
+
g[i] = @constraints[i][:from]
|
122
|
+
@logger.debug "g[#{i}] repaired as: #{g[i]}" if @logger
|
123
|
+
elsif g[i] > @constraints[i][:to]
|
124
|
+
range = "[#{@constraints[i][:from]},#{@constraints[i][:to]}]"
|
125
|
+
@logger.debug "repairing g[#{i}] #{g[i]} to fit within #{range}" if @logger
|
126
|
+
g[i] = @constraints[i][:to]
|
127
|
+
@logger.debug "g[#{i}] repaired as: #{g[i]}" if @logger
|
128
|
+
end
|
101
129
|
end
|
102
130
|
end
|
103
131
|
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require 'facter'
|
3
|
+
require 'logger'
|
4
|
+
require 'matrix'
|
5
|
+
|
6
|
+
require 'mhl/charged_swarm'
|
7
|
+
|
8
|
+
|
9
|
+
module MHL
|
10
|
+
# This solver implements the multiswarm QPSO algorithm, based on a number of
|
11
|
+
# charged (QPSO Type 2) and neutral (PSO) swarms.
|
12
|
+
#
|
13
|
+
# For more information, refer to:
|
14
|
+
# [BLACKWELLBRANKE04] Tim Blackwell, Jürgen Branke, "Multi-swarm Optimization
|
15
|
+
# in Dynamic Environments", Applications of Evolutionary Computing, pp.
|
16
|
+
# 489-500, Springer, 2004. DOI: 10.1007/978-3-540-24653-4_50
|
17
|
+
class MultiSwarmQPSOSolver
|
18
|
+
|
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
|
24
|
+
|
25
|
+
@num_swarms = opts[:num_swarms].to_i
|
26
|
+
unless @num_swarms
|
27
|
+
raise ArgumentError, 'Number of swarms is a required parameter!'
|
28
|
+
end
|
29
|
+
|
30
|
+
@random_position_func = opts[:random_position_func]
|
31
|
+
@random_velocity_func = opts[:random_velocity_func]
|
32
|
+
|
33
|
+
@start_positions = opts[:start_positions]
|
34
|
+
@exit_condition = opts[:exit_condition]
|
35
|
+
|
36
|
+
@pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
|
37
|
+
|
38
|
+
case opts[:logger]
|
39
|
+
when :stdout
|
40
|
+
@logger = Logger.new(STDOUT)
|
41
|
+
when :stderr
|
42
|
+
@logger = Logger.new(STDERR)
|
43
|
+
else
|
44
|
+
@logger = opts[:logger]
|
45
|
+
end
|
46
|
+
|
47
|
+
@quiet = opts[:quiet]
|
48
|
+
|
49
|
+
if @logger
|
50
|
+
@logger.level = opts[:log_level] or Logger::WARN
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# This is the method that solves the optimization problem
|
55
|
+
#
|
56
|
+
# Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
|
57
|
+
# object) that accepts the genotype as argument (that is, the set of
|
58
|
+
# parameters) and returns the phenotype (that is, the function result)
|
59
|
+
def solve(func, params={})
|
60
|
+
|
61
|
+
# setup particles
|
62
|
+
if @start_positions.nil?
|
63
|
+
swarms = Array.new(@num_swarms) do |index|
|
64
|
+
ChargedSwarm.new(@swarm_size,
|
65
|
+
Array.new(@swarm_size) { Vector[*@random_position_func.call] },
|
66
|
+
Array.new(@swarm_size / 2) { Vector[*@random_velocity_func.call] },
|
67
|
+
params)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
raise 'Unimplemented yet!'
|
71
|
+
# particles = @start_positions.each_slice(2).map do |pos,vel|
|
72
|
+
# { position: Vector[*pos] }
|
73
|
+
# end
|
74
|
+
end
|
75
|
+
|
76
|
+
# initialize variables
|
77
|
+
gen = 0
|
78
|
+
overall_best = nil
|
79
|
+
|
80
|
+
# default behavior is to loop forever
|
81
|
+
begin
|
82
|
+
gen += 1
|
83
|
+
@logger.info "MSQPSO - Starting generation #{gen}" if @logger
|
84
|
+
|
85
|
+
# create latch to control program termination
|
86
|
+
latch = Concurrent::CountDownLatch.new(@num_swarms * @swarm_size)
|
87
|
+
|
88
|
+
# assess height for every particle
|
89
|
+
swarms.each do |s|
|
90
|
+
s.each do |particle|
|
91
|
+
@pool.post do
|
92
|
+
# evaluate target function
|
93
|
+
particle.evaluate(func)
|
94
|
+
# update latch
|
95
|
+
latch.count_down
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# wait for all the evaluations to end
|
101
|
+
latch.wait
|
102
|
+
|
103
|
+
# update attractors (the highest particle in each swarm)
|
104
|
+
swarm_attractors = swarms.map {|s| s.update_attractor }
|
105
|
+
|
106
|
+
best_attractor = swarm_attractors.max_by {|x| x[:height] }
|
107
|
+
|
108
|
+
# print results
|
109
|
+
puts "> gen #{gen}, best: #{best_attractor[:position]}, #{best_attractor[:height]}" unless @quiet
|
110
|
+
|
111
|
+
# calculate overall best
|
112
|
+
if overall_best.nil?
|
113
|
+
overall_best = best_attractor
|
114
|
+
else
|
115
|
+
overall_best = [ overall_best, best_attractor ].max_by {|x| x[:height] }
|
116
|
+
end
|
117
|
+
|
118
|
+
# exclusion phase
|
119
|
+
# this phase is necessary to preserve diversity between swarms. we need
|
120
|
+
# to ensure that swarm attractors are distant at least r_{excl} units
|
121
|
+
# from each other. if the attractors of two swarms are closer than
|
122
|
+
# r_{excl}, we randomly reinitialize the worst of those swarms.
|
123
|
+
# TODO: IMPLEMENT
|
124
|
+
|
125
|
+
# anti-convergence phase
|
126
|
+
# this phase is necessary to ensure that a swarm is "spread" enough to
|
127
|
+
# effectively follow the movements of a "peak" in the solution space.
|
128
|
+
# TODO: IMPLEMENT
|
129
|
+
|
130
|
+
# mutate swarms
|
131
|
+
swarms.each {|s| s.mutate }
|
132
|
+
|
133
|
+
end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
|
134
|
+
|
135
|
+
overall_best
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|