mhl 0.2.0 → 0.3.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,44 @@
1
+ require 'mhl/rechenberg_controller'
2
+
3
+ module MHL
4
+ class HyperMutationPlusRechenbergController
5
+ DEFAULT_HM_GENERATIONS = 10
6
+
7
+ def initialize(params={})
8
+ @opts = { keep_for: DEFAULT_HM_GENERATIONS }.merge!(params)
9
+ @rc = RechenbergController.new
10
+ @gen = @gens_from_last_reset = 1
11
+ end
12
+
13
+ def call(solver, best)
14
+ if @pending_reset
15
+ # set mutation_probability to HM value
16
+ solver.mutation_probability = @pending_reset
17
+
18
+ # reinitialize controller
19
+ @rc = RechenbergController.new
20
+
21
+ # reinitialize counter of generations from last reset
22
+ @gens_from_last_reset = 0
23
+
24
+ # undefine pending_reset
25
+ # NOTE: not sure if we should we go as far as calling
26
+ # remove_instance_variable(:@pending_reset) here
27
+ @pending_reset = nil
28
+ end
29
+
30
+ # do nothing for the first @opts[:keep_for] generations
31
+ if @gens_from_last_reset > @opts[:keep_for]
32
+ @rc.call(solver, best)
33
+ end
34
+
35
+ # update counters
36
+ @gen += 1
37
+ @gens_from_last_reset += 1
38
+ end
39
+
40
+ def reset_mutation_probability(value)
41
+ @pending_reset = value
42
+ end
43
+ end
44
+ end
@@ -2,7 +2,7 @@ module MHL
2
2
 
3
3
  # This class implements a genotype with integer space representation
4
4
  class IntegerVectorGenotypeSpace
5
- def initialize(opts)
5
+ def initialize(opts, logger)
6
6
  @random_func = opts[:random_func]
7
7
 
8
8
  @dimensions = opts[:dimensions].to_i
@@ -13,9 +13,9 @@ module MHL
13
13
  # TODO: enable to choose which recombination function to use
14
14
  case opts[:recombination_type].to_s
15
15
  when /intermediate/i
16
- @recombination_func = :intermediate_recombination
16
+ @recombination_func = :extended_intermediate_recombination_int
17
17
  when /line/i
18
- @recombination_func = :line_recombination
18
+ @recombination_func = :extended_line_recombination_int
19
19
  else
20
20
  raise ArgumentError, 'Recombination function must be either line or intermediate!'
21
21
  end
@@ -25,7 +25,7 @@ module MHL
25
25
  raise ArgumentError, 'Constraints must be provided for every dimension!'
26
26
  end
27
27
 
28
- @logger = opts[:logger]
28
+ @logger = logger
29
29
  end
30
30
 
31
31
  def get_random
@@ -45,8 +45,8 @@ module MHL
45
45
  def reproduce_from(p1, p2, mutation_rv, recombination_rv)
46
46
  # make copies of p1 and p2
47
47
  # (we're only interested in the :genotype key)
48
- c1 = { :genotype => p1[:genotype].dup }
49
- c2 = { :genotype => p2[:genotype].dup }
48
+ c1 = { genotype: p1[:genotype].dup }
49
+ c2 = { genotype: p2[:genotype].dup }
50
50
 
51
51
  # mutation comes first
52
52
  random_delta_mutation(c1[:genotype], mutation_rv)
@@ -66,6 +66,8 @@ module MHL
66
66
 
67
67
  private
68
68
 
69
+ # mutation based on random perturbations of (all) the individual's
70
+ # chromosomes, according to a geometric distribution [TORTONESI16]
69
71
  def random_delta_mutation(g, mutation_rv)
70
72
  g.each_index do |i|
71
73
  delta = mutation_rv.next
@@ -80,7 +82,9 @@ module MHL
80
82
  end
81
83
  end
82
84
 
83
- def intermediate_recombination(g1, g2, recombination_rv)
85
+ # integer variant of extended intermediate recombination [MUHLENBEIN93]
86
+ # (see [LUKE15] page 65)
87
+ def extended_intermediate_recombination_int(g1, g2, recombination_rv)
84
88
  # TODO: disable this check in non-debugging mode
85
89
  raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
86
90
 
@@ -97,7 +101,9 @@ module MHL
97
101
  end
98
102
  end
99
103
 
100
- def line_recombination(g1, g2, recombination_rv)
104
+ # integer variant of extended line recombination [MUHLENBEIN93] (see
105
+ # [LUKE15] page 64)
106
+ def extended_line_recombination_int(g1, g2, recombination_rv)
101
107
  # TODO: disable this check in non-debugging mode
102
108
  raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
103
109
 
@@ -29,11 +29,10 @@ module MHL
29
29
  @random_position_func = opts[:random_position_func]
30
30
  @random_velocity_func = opts[:random_velocity_func]
31
31
 
32
- @start_positions = opts[:start_positions]
32
+ @start_positions = opts[:start_positions]
33
+ @start_velocities = opts[:start_velocities]
33
34
 
34
- @exit_condition = opts[:exit_condition]
35
-
36
- @pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
35
+ @exit_condition = opts[:exit_condition]
37
36
 
38
37
  case opts[:logger]
39
38
  when :stdout
@@ -47,7 +46,7 @@ module MHL
47
46
  @quiet = opts[:quiet]
48
47
 
49
48
  if @logger
50
- @logger.level = opts[:log_level] or Logger::WARN
49
+ @logger.level = (opts[:log_level] or :warn)
51
50
  end
52
51
  end
53
52
 
@@ -107,8 +106,9 @@ module MHL
107
106
  raise ArgumentError, "Not enough information to initialize particle velocities!"
108
107
  end
109
108
 
110
- ChargedSwarm.new(@swarm_size, init_pos, init_vel,
111
- params.merge(constraints: @constraints))
109
+ ChargedSwarm.new(size: @swarm_size, initial_positions: init_pos,
110
+ initial_velocities: init_vel,
111
+ constraints: @constraints, logger: @logger)
112
112
  end
113
113
 
114
114
  # initialize variables
@@ -120,31 +120,43 @@ module MHL
120
120
  iter += 1
121
121
  @logger.info "MultiSwarm QPSO - Starting iteration #{iter}" if @logger
122
122
 
123
- # create latch to control program termination
124
- latch = Concurrent::CountDownLatch.new(@num_swarms * @swarm_size)
125
-
126
123
  # assess height for every particle
127
- swarms.each do |s|
128
- s.each do |particle|
129
- @pool.post do
124
+ if params[:concurrent]
125
+ # the function to optimize is thread safe: call it multiple times in
126
+ # a concurrent fashion
127
+ # to this end, we use the high level promise-based construct
128
+ # recommended by the authors of ruby's (fantastic) concurrent gem
129
+ promises = swarms.map do |swarm|
130
+ swarm.map do |particle|
131
+ Concurrent::Promise.execute do
132
+ # evaluate target function
133
+ particle.evaluate(func)
134
+ end
135
+ end
136
+ end.flatten!
137
+
138
+ # wait for all the spawned threads to finish
139
+ promises.map(&:wait)
140
+ else
141
+ # the function to optimize is not thread safe: call it multiple times
142
+ # in a sequential fashion
143
+ swarms.each do |swarm|
144
+ swarm.each do |particle|
130
145
  # evaluate target function
131
146
  particle.evaluate(func)
132
- # update latch
133
- latch.count_down
134
147
  end
135
148
  end
136
149
  end
137
150
 
138
- # wait for all the evaluations to end
139
- latch.wait
140
-
141
151
  # update attractors (the highest particle in each swarm)
142
152
  swarm_attractors = swarms.map {|s| s.update_attractor }
143
153
 
144
154
  best_attractor = swarm_attractors.max_by {|x| x[:height] }
145
155
 
146
156
  # print results
147
- puts "> iter #{iter}, best: #{best_attractor[:position]}, #{best_attractor[:height]}" unless @quiet
157
+ if @logger and !@quiet
158
+ @logger.info "> iter #{iter}, best: #{best_attractor[:position]}, #{best_attractor[:height]}"
159
+ end
148
160
 
149
161
  # calculate overall best
150
162
  if overall_best.nil?
@@ -26,8 +26,6 @@ module MHL
26
26
 
27
27
  @exit_condition = opts[:exit_condition]
28
28
 
29
- @pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
30
-
31
29
  case opts[:logger]
32
30
  when :stdout
33
31
  @logger = Logger.new(STDOUT)
@@ -40,7 +38,7 @@ module MHL
40
38
  @quiet = opts[:quiet]
41
39
 
42
40
  if @logger
43
- @logger.level = (opts[:log_level] or Logger::WARN)
41
+ @logger.level = (opts[:log_level] or :warn)
44
42
  end
45
43
  end
46
44
 
@@ -100,8 +98,9 @@ module MHL
100
98
  end
101
99
 
102
100
  # setup particles
103
- swarm = PSOSwarm.new(@swarm_size, init_pos, init_vel,
104
- params.merge(constraints: @constraints))
101
+ swarm = PSOSwarm.new(size: @swarm_size, initial_positions: init_pos,
102
+ initial_velocities: init_vel,
103
+ constraints: @constraints, logger: @logger)
105
104
 
106
105
  # initialize variables
107
106
  iter = 0
@@ -112,27 +111,37 @@ module MHL
112
111
  iter += 1
113
112
  @logger.info("PSO - Starting iteration #{iter}") if @logger
114
113
 
115
- # create latch to control program termination
116
- latch = Concurrent::CountDownLatch.new(@swarm_size)
117
-
118
114
  # assess height for every particle
119
- swarm.each do |particle|
120
- @pool.post do
115
+ if params[:concurrent]
116
+ # the function to optimize is thread safe: call it multiple times in
117
+ # a concurrent fashion
118
+ # to this end, we use the high level promise-based construct
119
+ # recommended by the authors of ruby's (fantastic) concurrent gem
120
+ promises = swarm.map do |particle|
121
+ Concurrent::Promise.execute do
122
+ # evaluate target function
123
+ particle.evaluate(func)
124
+ end
125
+ end
126
+
127
+ # wait for all the spawned threads to finish
128
+ promises.map(&:wait)
129
+ else
130
+ # the function to optimize is not thread safe: call it multiple times
131
+ # in a sequential fashion
132
+ swarm.each do |particle|
121
133
  # evaluate target function
122
134
  particle.evaluate(func)
123
- # update latch
124
- latch.count_down
125
135
  end
126
136
  end
127
137
 
128
- # wait for all the threads to terminate
129
- latch.wait
130
-
131
138
  # get swarm attractor (the highest particle)
132
139
  swarm_attractor = swarm.update_attractor
133
140
 
134
141
  # print results
135
- puts "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
142
+ if @logger and !@quiet
143
+ @logger.info "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}"
144
+ end
136
145
 
137
146
  # calculate overall best (that plays the role of swarm attractor)
138
147
  if overall_best.nil?
@@ -5,7 +5,8 @@ require 'mhl/particle'
5
5
  module MHL
6
6
  class PSOSwarm < GenericSwarmBehavior
7
7
 
8
- def initialize(size, initial_positions, initial_velocities, params={})
8
+ def initialize(size:, initial_positions:, initial_velocities:,
9
+ c1: nil, c2: nil, chi: nil, constraints: nil, logger: nil)
9
10
  @size = size
10
11
  @particles = Array.new(@size) do |index|
11
12
  Particle.new(initial_positions[index], initial_velocities[index])
@@ -14,21 +15,22 @@ module MHL
14
15
  @iteration = 1
15
16
 
16
17
  # get values for parameters C1 and C2
17
- @c1 = (params[:c1] || DEFAULT_C1).to_f
18
- @c2 = (params[:c1] || DEFAULT_C2).to_f
18
+ @c1 = (c1 || DEFAULT_C1).to_f
19
+ @c2 = (c2 || DEFAULT_C2).to_f
19
20
 
20
21
  # 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]
22
+ @get_chi = if chi and chi.respond_to? :call
23
+ chi
23
24
  else
24
- ->(iter) { (params[:chi] || DEFAULT_CHI).to_f }
25
+ ->(iter) { (chi || DEFAULT_CHI).to_f }
25
26
  end
26
27
 
27
- if params.has_key? :constraints
28
- puts "PSOSwarm called w/ constraints: #{params[:constraints]}"
29
- end
28
+ @constraints = constraints
29
+ @logger = logger
30
30
 
31
- @constraints = params[:constraints]
31
+ if @constraints and @logger
32
+ @logger.info "PSOSwarm called w/ constraints: #{@constraints}"
33
+ end
32
34
  end
33
35
 
34
36
  def mutate(params={})
@@ -5,29 +5,30 @@ require 'mhl/quantum_particle'
5
5
  module MHL
6
6
  class QPSOSwarm < GenericSwarmBehavior
7
7
 
8
- def initialize(size, initial_positions, params={})
8
+ def initialize(size:, initial_positions:, alpha: nil, constraints: nil, logger: nil)
9
9
  @size = size
10
10
  @particles = Array.new(@size) do |index|
11
11
  QuantumParticle.new(initial_positions[index])
12
12
  end
13
13
 
14
14
  # find problem dimension
15
- @dimension = initial_positions[0].size
15
+ @dimension = initial_positions[0].size
16
16
 
17
17
  @iteration = 1
18
18
 
19
19
  # define procedure to get dynamic value for alpha
20
- @get_alpha = if params.has_key? :alpha and params[:alpha].respond_to? :call
21
- params[:alpha]
20
+ @get_alpha = if alpha and alpha.respond_to? :call
21
+ alpha
22
22
  else
23
- ->(it) { (params[:alpha] || DEFAULT_ALPHA).to_f }
23
+ ->(it) { (alpha || DEFAULT_ALPHA).to_f }
24
24
  end
25
25
 
26
- if params.has_key? :constraints
27
- puts "QPSOSwarm called w/ constraints: #{params[:constraints]}"
28
- end
26
+ @constraints = constraints
27
+ @logger = logger
29
28
 
30
- @constraints = params[:constraints]
29
+ if @constraints and @logger
30
+ @logger.info "QPSOSwarm called w/ constraints: #{@constraints}"
31
+ end
31
32
  end
32
33
 
33
34
  def mutate
@@ -27,8 +27,6 @@ module MHL
27
27
 
28
28
  @exit_condition = opts[:exit_condition]
29
29
 
30
- @pool = Concurrent::FixedThreadPool.new(Concurrent::processor_count * 4)
31
-
32
30
  case opts[:logger]
33
31
  when :stdout
34
32
  @logger = Logger.new(STDOUT)
@@ -41,7 +39,7 @@ module MHL
41
39
  @quiet = opts[:quiet]
42
40
 
43
41
  if @logger
44
- @logger.level = opts[:log_level] or Logger::WARN
42
+ @logger.level = (opts[:log_level] or :warn)
45
43
  end
46
44
  end
47
45
 
@@ -66,7 +64,7 @@ module MHL
66
64
  min = @constraints[:min]
67
65
  max = @constraints[:max]
68
66
  # randomization is independent along each dimension
69
- random_pos = min.zip(max).map do |min_i,max_i|
67
+ min.zip(max).map do |min_i,max_i|
70
68
  min_i + SecureRandom.random_number * (max_i - min_i)
71
69
  end
72
70
  end
@@ -74,8 +72,8 @@ module MHL
74
72
  raise ArgumentError, "Not enough information to initialize particle positions!"
75
73
  end
76
74
 
77
- swarm = QPSOSwarm.new(@swarm_size, init_pos,
78
- params.merge(constraints: @constraints))
75
+ swarm = QPSOSwarm.new(size: @swarm_size, initial_positions: init_pos,
76
+ constraints: @constraints, logger: @logger)
79
77
 
80
78
  # initialize variables
81
79
  iter = 0
@@ -86,26 +84,36 @@ module MHL
86
84
  iter += 1
87
85
  @logger.info "QPSO - Starting iteration #{iter}" if @logger
88
86
 
89
- # create latch to control program termination
90
- latch = Concurrent::CountDownLatch.new(@swarm_size)
87
+ if params[:concurrent]
88
+ # the function to optimize is thread safe: call it multiple times in
89
+ # a concurrent fashion
90
+ # to this end, we use the high level promise-based construct
91
+ # recommended by the authors of ruby's (fantastic) concurrent gem
92
+ promises = swarm.map do |particle|
93
+ Concurrent::Promise.execute do
94
+ # evaluate target function
95
+ particle.evaluate(func)
96
+ end
97
+ end
91
98
 
92
- swarm.each do |particle|
93
- @pool.post do
99
+ # wait for all the spawned threads to finish
100
+ promises.map(&:wait)
101
+ else
102
+ # the function to optimize is not thread safe: call it multiple times
103
+ # in a sequential fashion
104
+ swarm.each do |particle|
94
105
  # evaluate target function
95
106
  particle.evaluate(func)
96
- # update latch
97
- latch.count_down
98
107
  end
99
108
  end
100
109
 
101
- # wait for all the evaluations to end
102
- latch.wait
103
-
104
110
  # get swarm attractor (the highest particle)
105
111
  swarm_attractor = swarm.update_attractor
106
112
 
107
113
  # print results
108
- puts "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}" unless @quiet
114
+ if @logger and !@quiet
115
+ @logger.info "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}"
116
+ end
109
117
 
110
118
  # calculate overall best (that plays the role of swarm attractor)
111
119
  if overall_best.nil?