mhl 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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(Facter.value(:processorcount).to_i * 4)
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(Facter.value(:processorcount).to_i * 4)
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
- # 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)
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
- else
70
- raise 'Unimplemented yet!'
71
- # particles = @start_positions.each_slice(2).map do |pos,vel|
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
- gen = 0
115
+ iter = 0
78
116
  overall_best = nil
79
117
 
80
118
  # default behavior is to loop forever
81
119
  begin
82
- gen += 1
83
- @logger.info "MSQPSO - Starting generation #{gen}" if @logger
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 "> gen #{gen}, best: #{best_attractor[:position]}, #{best_attractor[:height]}" unless @quiet
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(gen, overall_best)
171
+ end while @exit_condition.nil? or !@exit_condition.call(iter, overall_best)
134
172
 
135
173
  overall_best
136
174
  end
@@ -10,23 +10,58 @@ module MHL
10
10
  end
11
11
 
12
12
  # move particle and update attractor
13
- def move(omega, c1, c2, swarm_attractor)
13
+ def move(chi, c1, c2, swarm_attractor)
14
14
  raise 'Particle attractor is nil!' if @attractor.nil?
15
- # raise 'Swarm attractor is nil!' if swarm_attractor.nil?
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
- # previous velocity is damped by inertia weight omega
20
- omega * @velocity +
21
- # "memory" component (linear attraction towards the best position
22
- # that this particle encountered so far)
23
- c1 * SecureRandom.random_number * (attractor[:position] - @position) +
24
- # "social" component (linear attraction towards the best position
25
- # that the entire swarm encountered so far)
26
- c2 * SecureRandom.random_number * (swarm_attractor[:position] - @position)
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 + @velocity
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 PSO with inertia weight variant algorithm.
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
- unless @swarm_size
22
- raise ArgumentError, 'Swarm size is a required parameter!'
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 = opts[: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(Facter.value(:processorcount).to_i * 4)
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
- # setup particles
56
- if @start_positions.nil?
57
- swarm = PSOSwarm.new(@swarm_size,
58
- Array.new(@swarm_size) { Vector[*@random_position_func.call] },
59
- Array.new(@swarm_size) { Vector[*@random_velocity_func.call] },
60
- params)
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
- # we only support the definition of start positions - not velocities
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
- gen = 0
107
+ iter = 0
74
108
  overall_best = nil
75
109
 
76
110
  # default behavior is to loop forever
77
111
  begin
78
- gen += 1
79
- @logger.info("PSO - Starting generation #{gen}") if @logger
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 "> gen #{gen}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
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(gen, overall_best)
147
+ end while @exit_condition.nil? or !@exit_condition.call(iter, overall_best)
114
148
 
115
149
  overall_best
116
150
  end
@@ -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
- @generation = 1
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 omega
24
- @get_omega = if params.has_key? :omega and params[:omega].respond_to? :call
25
- params[:omega]
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
- ->(gen) { (params[:omega] || DEFAULT_OMEGA).to_f }
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/ :constraints => #{params[:constraints]}"
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 omega parameter
39
- omega = @get_omega.call(@generation)
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(omega, @c1, @c2, @swarm_attractor)
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
- @generation += 1
46
+ @iteration += 1
50
47
  end
51
48
  end
52
49
  end
@@ -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
- @generation = 1
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
- ->(gen) { (params[:alpha] || DEFAULT_ALPHA).to_f }
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(@generation)
35
+ alpha = @get_alpha.call(@iteration)
33
36
 
34
- # this calculates the C_n parameter (basically, the centroid of the set
35
- # of all the particle attractors) as defined in [SUN11], formulae 4.81
36
- # and 4.82
37
- c_n = @particles.inject(Vector[*[0]*@dimension]) {|s,p| s += p.attractor[:position] } / @size.to_f
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
- @particles.each { |p| p.move(alpha, c_n, @swarm_attractor) }
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
- @generation += 1
52
+ @iteration += 1
42
53
  end
43
54
 
44
55
  end