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