mhl 0.0.1 → 0.1.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.
@@ -0,0 +1,33 @@
1
+ require 'securerandom'
2
+
3
+ require 'mhl/generic_particle'
4
+
5
+ module MHL
6
+ class Particle < GenericParticle
7
+ def initialize(initial_position, initial_velocity)
8
+ super(initial_position)
9
+ @velocity = initial_velocity
10
+ end
11
+
12
+ # move particle and update attractor
13
+ def move(omega, c1, c2, swarm_attractor)
14
+ raise 'Particle attractor is nil!' if @attractor.nil?
15
+ # raise 'Swarm attractor is nil!' if swarm_attractor.nil?
16
+
17
+ # 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
+
28
+ # update position
29
+ @position = @position + @velocity
30
+ end
31
+
32
+ end
33
+ end
@@ -1,8 +1,19 @@
1
- require 'matrix'
2
- require 'securerandom'
1
+ require 'concurrent'
2
+ require 'facter'
3
+ require 'logger'
4
+
5
+ require 'mhl/pso_swarm'
6
+
3
7
 
4
8
  module MHL
5
9
 
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
6
17
  class ParticleSwarmOptimizationSolver
7
18
 
8
19
  def initialize(opts={})
@@ -16,6 +27,23 @@ module MHL
16
27
 
17
28
  @start_positions = opts[:start_positions]
18
29
  @exit_condition = opts[:exit_condition]
30
+
31
+ @pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
32
+
33
+ case opts[:logger]
34
+ when :stdout
35
+ @logger = Logger.new(STDOUT)
36
+ when :stderr
37
+ @logger = Logger.new(STDERR)
38
+ else
39
+ @logger = opts[:logger]
40
+ end
41
+
42
+ @quiet = opts[:quiet]
43
+
44
+ if @logger
45
+ @logger.level = (opts[:log_level] or Logger::WARN)
46
+ end
19
47
  end
20
48
 
21
49
  # This is the method that solves the optimization problem
@@ -23,99 +51,69 @@ module MHL
23
51
  # Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
24
52
  # object) that accepts the genotype as argument (that is, the set of
25
53
  # parameters) and returns the phenotype (that is, the function result)
26
- def solve(func)
54
+ def solve(func, params={})
27
55
  # setup particles
28
56
  if @start_positions.nil?
29
- particles = Array.new(@swarm_size) do
30
- { position: Vector[*@random_position_func.call], velocity: Vector[*@random_velocity_func.call] }
31
- end
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)
32
61
  else
33
- particles = @start_positions.each_slice(2).map do |pos,vel|
34
- { position: Vector[*pos], velocity: Vector[*vel] }
35
- end
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
36
70
  end
37
71
 
38
72
  # initialize variables
39
73
  gen = 0
40
74
  overall_best = nil
41
75
 
42
- # completely made up values
43
- alpha = 0.5
44
- beta = 0.3
45
- gamma = 0.7
46
- delta = 0.5
47
- epsilon = 0.6
48
-
49
76
  # default behavior is to loop forever
50
77
  begin
51
78
  gen += 1
52
- puts "Starting generation #{gen} at #{Time.now}"
79
+ @logger.info("PSO - Starting generation #{gen}") if @logger
53
80
 
54
- # assess height for every particle
55
- particles.each do |p|
56
- p[:task] = Concurrent::Future.new { func.call(p[:position]) }
57
- end
81
+ # create latch to control program termination
82
+ latch = Concurrent::CountDownLatch.new(@swarm_size)
58
83
 
59
- # wait for all the evaluations to end
60
- particles.each_with_index do |p,i|
61
- p[:height] = p[:task].value
62
- if p[:highest_value].nil? or p[:height] > p[:highest_value]
63
- p[:highest_value] = p[:height]
64
- p[:highest_position] = p[:position]
84
+ # assess height for every particle
85
+ swarm.each do |particle|
86
+ @pool.post do
87
+ # evaluate target function
88
+ particle.evaluate(func)
89
+ # update latch
90
+ latch.count_down
65
91
  end
66
92
  end
67
93
 
68
- # find highest particle
69
- highest_particle = particles.max_by {|x| x[:height] }
94
+ # wait for all the threads to terminate
95
+ latch.wait
70
96
 
71
- # calculate overall best
97
+ # get swarm attractor (the highest particle)
98
+ swarm_attractor = swarm.update_attractor
99
+
100
+ # print results
101
+ puts "> gen #{gen}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
102
+
103
+ # calculate overall best (that plays the role of swarm attractor)
72
104
  if overall_best.nil?
73
- overall_best = highest_particle
105
+ overall_best = swarm_attractor
74
106
  else
75
- overall_best = [ overall_best, highest_particle ].max_by {|x| x[:height] }
107
+ overall_best = [ overall_best, swarm_attractor ].max_by {|x| x[:height] }
76
108
  end
77
109
 
78
110
  # mutate swarm
79
- particles.each do |p|
80
- # randomly sample particles and use them as informants
81
- informants = random_portion(particles)
82
-
83
- # make sure that p is included among the informants
84
- informants << p unless informants.include? p
85
-
86
- # get fittest informant
87
- fittest_informant = informants.max_by {|x| x[:height] }
88
-
89
- # update velocity
90
- p[:velocity] =
91
- alpha * p[:velocity] +
92
- beta * (p[:highest_position] - p[:position]) +
93
- gamma * (fittest_informant[:highest_position] - p[:position]) +
94
- delta * (overall_best[:highest_position] - p[:position])
95
-
96
- # update position
97
- p[:position] = p[:position] + epsilon * p[:velocity]
98
- end
111
+ swarm.mutate
99
112
 
100
113
  end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
101
- end
102
-
103
- private
104
-
105
- def random_portion(array, ratio=0.1)
106
- # get size of random array to return
107
- size = (ratio * array.size).ceil
108
-
109
- (1..size).inject([]) do |acc,i|
110
- # randomly sample a new element
111
- begin
112
- new_element = array[SecureRandom.random_number(array.size)]
113
- end while acc.include? new_element
114
114
 
115
- # insert element in the accumulator
116
- acc << new_element
117
- end
118
- end
115
+ overall_best
116
+ end
119
117
 
120
118
  end
121
119
 
@@ -0,0 +1,52 @@
1
+ require 'matrix'
2
+ require 'securerandom'
3
+
4
+ require 'mhl/generic_swarm'
5
+ require 'mhl/particle'
6
+
7
+
8
+ module MHL
9
+ class PSOSwarm < GenericSwarmBehavior
10
+
11
+ def initialize(size, initial_positions, initial_velocities, params={})
12
+ @size = size
13
+ @particles = Array.new(@size) do |index|
14
+ Particle.new(initial_positions[index], initial_velocities[index])
15
+ end
16
+
17
+ @generation = 1
18
+
19
+ # get values for parameters C1 and C2
20
+ @c1 = (params[:c1] || DEFAULT_C1).to_f
21
+ @c2 = (params[:c1] || DEFAULT_C2).to_f
22
+
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]
26
+ else
27
+ ->(gen) { (params[:omega] || DEFAULT_OMEGA).to_f }
28
+ end
29
+
30
+ if params.has_key? :constraints
31
+ puts "PSOSwarm called w/ :constraints => #{params[:constraints]}"
32
+ end
33
+
34
+ @constraints = params[:constraints]
35
+ end
36
+
37
+ def mutate(params={})
38
+ # get omega parameter
39
+ omega = @get_omega.call(@generation)
40
+
41
+ # move particles
42
+ @particles.each_with_index do |p,i|
43
+ p.move(omega, @c1, @c2, @swarm_attractor)
44
+ if @constraints
45
+ p.remain_within(@constraints)
46
+ end
47
+ end
48
+
49
+ @generation += 1
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ require 'matrix'
2
+ require 'securerandom'
3
+
4
+ require 'mhl/generic_swarm'
5
+ require 'mhl/quantum_particle'
6
+
7
+
8
+ module MHL
9
+ class QPSOSwarm < GenericSwarmBehavior
10
+
11
+ def initialize(size, initial_positions, params={})
12
+ @size = size
13
+ @particles = Array.new(@size) do |index|
14
+ QuantumParticle.new(initial_positions[index])
15
+ end
16
+
17
+ # find problem dimension
18
+ @dimension = initial_positions[0].size
19
+
20
+ @generation = 1
21
+
22
+ # define procedure to get dynamic value for alpha
23
+ @get_alpha = if params.has_key? :alpha and params[:alpha].respond_to? :call
24
+ params[:alpha]
25
+ else
26
+ ->(gen) { (params[:alpha] || DEFAULT_ALPHA).to_f }
27
+ end
28
+ end
29
+
30
+ def mutate
31
+ # get alpha parameter
32
+ alpha = @get_alpha.call(@generation)
33
+
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
38
+
39
+ @particles.each { |p| p.move(alpha, c_n, @swarm_attractor) }
40
+
41
+ @generation += 1
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,40 @@
1
+ require 'matrix'
2
+ require 'securerandom'
3
+
4
+ require 'mhl/generic_particle'
5
+
6
+ module MHL
7
+ class QuantumParticle < GenericParticle
8
+ attr_reader :position
9
+
10
+ # move particle using QPSO - Type II algorithm
11
+ def move(alpha, mean_best, swarm_attractor)
12
+ raise 'Particle attractor is nil!' if @attractor.nil?
13
+ # raise 'Swarm attractor is nil!' if swarm_attractor.nil?
14
+
15
+ dimension = @position.size
16
+
17
+ # phi represents the \phi_{i,n} parameter in [SUN11], formula 4.83
18
+ phi = Array.new(dimension) { SecureRandom.random_number }
19
+
20
+ # p_i represents the p_{i,n} parameter in [SUN11], formulae 4.82 and 4.83
21
+ p_i =
22
+ Vector[*phi.zip(@attractor[:position]).map {|phi_j,p_j| phi_j * p_j }] +
23
+ Vector[*phi.zip(swarm_attractor[:position]).map {|phi_j,g_j| (1.0 - phi_j) * g_j }]
24
+
25
+ # delta represents the displacement for the current position.
26
+ # See [SUN11], formula 4.82
27
+ delta =
28
+ @position.zip(mean_best).map {|x,y| alpha * (x-y).abs }. # \alpha * | X_{i,n} - C_n |
29
+ map {|x| x * Math.log(1.0 / SecureRandom.random_number) } # log(\frac{1}{u_{i,n+1}})
30
+
31
+ # update position
32
+ if SecureRandom.random_number < 0.5
33
+ @position = p_i + Vector[*delta]
34
+ else
35
+ @position = p_i - Vector[*delta]
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,113 @@
1
+ require 'concurrent'
2
+ require 'facter'
3
+ require 'logger'
4
+ require 'matrix'
5
+ require 'securerandom'
6
+
7
+ require 'mhl/qpso_swarm'
8
+
9
+
10
+ module MHL
11
+
12
+ # This solver implements the QPSO Type 2 algorithm.
13
+ #
14
+ # For more information, refer to equation 4.82 of:
15
+ # [SUN11] Jun Sun, Choi-Hong Lai, Xiao-Jun Wu, "Particle Swarm Optimisation:
16
+ # Classical and Quantum Perspectives", CRC Press, 2011
17
+ class QuantumPSOSolver
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
+ @random_position_func = opts[:random_position_func]
26
+
27
+ @start_positions = opts[:start_positions]
28
+ @exit_condition = opts[:exit_condition]
29
+
30
+ @pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
31
+
32
+ case opts[:logger]
33
+ when :stdout
34
+ @logger = Logger.new(STDOUT)
35
+ when :stderr
36
+ @logger = Logger.new(STDERR)
37
+ else
38
+ @logger = opts[:logger]
39
+ end
40
+
41
+ @quiet = opts[:quiet]
42
+
43
+ if @logger
44
+ @logger.level = opts[:log_level] or Logger::WARN
45
+ end
46
+ end
47
+
48
+ # This is the method that solves the optimization problem
49
+ #
50
+ # Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
51
+ # object) that accepts the genotype as argument (that is, the set of
52
+ # parameters) and returns the phenotype (that is, the function result)
53
+ def solve(func, params={})
54
+ # setup swarm
55
+ if @start_positions.nil?
56
+ swarm = QPSOSwarm.new(@swarm_size,
57
+ Array.new(@swarm_size) { Vector[*@random_position_func.call] },
58
+ params)
59
+ else
60
+ raise 'Unimplemented yet!'
61
+ # particles = @start_positions.map do |pos|
62
+ # { position: Vector[*pos] }
63
+ # end
64
+ end
65
+
66
+ # initialize variables
67
+ gen = 0
68
+ overall_best = nil
69
+
70
+ # default behavior is to loop forever
71
+ begin
72
+ gen += 1
73
+ @logger.info "QPSO - Starting generation #{gen}" if @logger
74
+
75
+ # create latch to control program termination
76
+ latch = Concurrent::CountDownLatch.new(@swarm_size)
77
+
78
+ swarm.each do |particle|
79
+ @pool.post do
80
+ # evaluate target function
81
+ particle.evaluate(func)
82
+ # update latch
83
+ latch.count_down
84
+ end
85
+ end
86
+
87
+ # wait for all the evaluations to end
88
+ latch.wait
89
+
90
+ # get swarm attractor (the highest particle)
91
+ swarm_attractor = swarm.update_attractor
92
+
93
+ # print results
94
+ puts "> gen #{gen}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
95
+
96
+ # calculate overall best (that plays the role of swarm attractor)
97
+ if overall_best.nil?
98
+ overall_best = swarm_attractor
99
+ else
100
+ overall_best = [ overall_best, swarm_attractor ].max_by {|x| x[:height] }
101
+ end
102
+
103
+ # mutate swarm
104
+ swarm.mutate
105
+
106
+ end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
107
+
108
+ overall_best
109
+ end
110
+
111
+ end
112
+
113
+ end