class MHL::ParticleSwarmOptimizationSolver

This solver implements the “canonical” variant of PSO called Constrained PSO. For more information, refer to equation 4.30 of [SUN11].

Constants

DEFAULT_SWARM_SIZE

This is the default swarm size recommended by SPSO 2011 [CLERC12].

Public Class Methods

new(opts={}) click to toggle source
# File lib/mhl/particle_swarm_optimization_solver.rb, line 16
def initialize(opts={})
  @swarm_size = opts[:swarm_size].try(:to_i) || DEFAULT_SWARM_SIZE

  @constraints = opts[:constraints]

  @random_position_func = opts[:random_position_func]
  @random_velocity_func = opts[:random_velocity_func]

  @start_positions  = opts[:start_positions]
  @start_velocities = opts[:start_velocities]

  @exit_condition  = opts[:exit_condition]

  case opts[:logger]
  when :stdout
    @logger = Logger.new(STDOUT)
  when :stderr
    @logger = Logger.new(STDERR)
  else
    @logger = opts[:logger]
  end

  @quiet = opts[:quiet]

  if @logger
    @logger.level = (opts[:log_level] or :warn)
  end
end

Public Instance Methods

solve(func, params={}) click to toggle source

This is the method that solves the optimization problem

Parameter func is supposed to be a method (or a Proc, a lambda, or any callable object) that accepts the genotype as argument (that is, the set of parameters) and returns the phenotype (that is, the function result)

# File lib/mhl/particle_swarm_optimization_solver.rb, line 50
def solve(func, params={})

  # initialize particle positions
  init_pos = if @start_positions
    # start positions have the highest priority
    @start_positions
  elsif @random_position_func
    # random_position_func has the second highest priority
    Array.new(@swarm_size) { @random_position_func.call }
  elsif @constraints
    # constraints were given, so we use them to initialize particle
    # positions. to this end, we adopt the SPSO 2006-2011 random position
    # initialization algorithm [CLERC12].
    Array.new(@swarm_size) do
      min = @constraints[:min]
      max = @constraints[:max]
      # randomization is independent along each dimension
      min.zip(max).map do |min_i,max_i|
        min_i + SecureRandom.random_number * (max_i - min_i)
      end
    end
  else
    raise ArgumentError, "Not enough information to initialize particle positions!"
  end

  # initialize particle velocities
  init_vel = if @start_velocities
    # start velocities have the highest priority
    @start_velocities
  elsif @random_velocity_func
    # random_velocity_func has the second highest priority
    Array.new(@swarm_size) { @random_velocity_func.call }
  elsif @constraints
    # constraints were given, so we use them to initialize particle
    # velocities. to this end, we adopt the SPSO 2011 random velocity
    # initialization algorithm [CLERC12].
    init_pos.map do |p|
      min = @constraints[:min]
      max = @constraints[:max]
      # randomization is independent along each dimension
      p.zip(min,max).map do |p_i,min_i,max_i|
        min_vel = min_i - p_i
        max_vel = max_i - p_i
        min_vel + SecureRandom.random_number * (max_vel - min_vel)
      end
    end
  else
    raise ArgumentError, "Not enough information to initialize particle velocities!"
  end

  # setup particles
  swarm = PSOSwarm.new(size: @swarm_size, initial_positions: init_pos,
                       initial_velocities: init_vel,
                       constraints: @constraints, logger: @logger)

  # initialize variables
  iter = 0
  overall_best = nil

  # default behavior is to loop forever
  begin
    iter += 1
    @logger.info("PSO - Starting iteration #{iter}") if @logger

    # assess height for every particle
    if params[:concurrent]
      # the function to optimize is thread safe: call it multiple times in
      # a concurrent fashion
      # to this end, we use the high level promise-based construct
      # recommended by the authors of ruby's (fantastic) concurrent gem
      promises = swarm.map do |particle|
        Concurrent::Promise.execute do
          # evaluate target function
          particle.evaluate(func)
        end
      end

      # wait for all the spawned threads to finish
      promises.map(&:wait)
    else
      # the function to optimize is not thread safe: call it multiple times
      # in a sequential fashion
      swarm.each do |particle|
        # evaluate target function
        particle.evaluate(func)
      end
    end

    # get swarm attractor (the highest particle)
    swarm_attractor = swarm.update_attractor

    # print results
    if @logger and !@quiet
      @logger.info "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}"
    end

    # calculate overall best (that plays the role of swarm attractor)
    if overall_best.nil?
      overall_best = swarm_attractor
    else
      overall_best = [ overall_best, swarm_attractor ].max_by {|x| x[:height] }
    end

    # mutate swarm
    swarm.mutate

  end while @exit_condition.nil? or !@exit_condition.call(iter, overall_best)

  overall_best
end