Some checks failed
		
		
	
	Run unit tests / rubocop (pull_request) Failing after 2m17s
				
			Run unit tests / check-licenses (pull_request) Successful in 3m51s
				
			Run unit tests / copyright_notice (pull_request) Successful in 4m4s
				
			Run unit tests / unit_tests (pull_request) Successful in 10m37s
				
			Run unit tests / build-static-assets (pull_request) Failing after 56s
				
			
		
			
				
	
	
		
			114 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			114 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
# Copyright (C) 2024-2025 LibreWeddingPlanner contributors
 | 
						|
 | 
						|
# frozen_string_literal: true
 | 
						|
 | 
						|
module VNS
 | 
						|
  class Engine
 | 
						|
    PERTURBATION_SIZES = [1, 1, 1, 2, 2, 3].freeze
 | 
						|
    ITERATIONS = 50
 | 
						|
    class << self
 | 
						|
      def sequence(elements)
 | 
						|
        elements = elements.to_a
 | 
						|
        (elements + elements.reverse).chunk(&:itself).map(&:first)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    def initialize
 | 
						|
      @perturbations = Set.new
 | 
						|
    end
 | 
						|
 | 
						|
    def target_function(&function)
 | 
						|
      @target_function = function
 | 
						|
    end
 | 
						|
 | 
						|
    def add_optimization(klass)
 | 
						|
      @optimizations ||= Set.new
 | 
						|
      @optimizations << klass
 | 
						|
    end
 | 
						|
 | 
						|
    def add_perturbation(klass)
 | 
						|
      @perturbations << klass
 | 
						|
    end
 | 
						|
 | 
						|
    def notify_progress(&block)
 | 
						|
      @progress_notifier = block
 | 
						|
    end
 | 
						|
 | 
						|
    def on_better_solution(&block)
 | 
						|
      @better_solution_notifier = block
 | 
						|
    end
 | 
						|
 | 
						|
    attr_writer :initial_solution
 | 
						|
 | 
						|
    def run
 | 
						|
      check_preconditions!
 | 
						|
 | 
						|
      @current_solution = @initial_solution
 | 
						|
      @best_score = @target_function.call(@current_solution)
 | 
						|
 | 
						|
      run_all_optimizations
 | 
						|
 | 
						|
      @progress_notifier&.call(Rational(1, ITERATIONS + 1))
 | 
						|
 | 
						|
      best_solution = @current_solution
 | 
						|
 | 
						|
      (1..ITERATIONS).each do |iteration|
 | 
						|
        @current_solution = Tables::WheelSwap.new(best_solution).call(PERTURBATION_SIZES.sample)
 | 
						|
        @best_score = @target_function.call(@current_solution)
 | 
						|
        Rails.logger.debug { "After perturbation: #{@best_score}" }
 | 
						|
 | 
						|
        run_all_optimizations
 | 
						|
 | 
						|
        @progress_notifier&.call(Rational(iteration + 1, ITERATIONS + 1))
 | 
						|
 | 
						|
        next unless best_solution.discomfort > @current_solution.discomfort
 | 
						|
 | 
						|
        best_solution = @current_solution
 | 
						|
        @better_solution_notifier&.call(best_solution)
 | 
						|
 | 
						|
        Rails.logger.debug do
 | 
						|
          "Found better solution after perturbation optimization: #{@current_solution.discomfort}"
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      best_solution
 | 
						|
    end
 | 
						|
 | 
						|
    private
 | 
						|
 | 
						|
    def check_preconditions!
 | 
						|
      raise 'No target function defined' unless @target_function
 | 
						|
      raise 'No optimizations defined' unless @optimizations
 | 
						|
      raise 'No initial solution defined' unless @initial_solution
 | 
						|
    end
 | 
						|
 | 
						|
    def run_all_optimizations
 | 
						|
      self.class.sequence(@optimizations).each do |optimization|
 | 
						|
        optimize(optimization)
 | 
						|
        Rails.logger.debug { "Finished optimization phase: #{optimization}" }
 | 
						|
      end
 | 
						|
      Rails.logger.debug { 'Finished all optimization phases' }
 | 
						|
    end
 | 
						|
 | 
						|
    def optimize(optimization_klass)
 | 
						|
      loop do
 | 
						|
        optimized = false
 | 
						|
 | 
						|
        optimization_klass.new(@current_solution).each do |alternative_solution|
 | 
						|
          score = @target_function.call(alternative_solution)
 | 
						|
          next if score >= @best_score
 | 
						|
 | 
						|
          @current_solution = alternative_solution.deep_dup
 | 
						|
          @best_score = score
 | 
						|
          optimized = true
 | 
						|
          Rails.logger.debug { "[#{optimization_klass}] Found better solution with score: #{score}" }
 | 
						|
 | 
						|
          break
 | 
						|
        end
 | 
						|
 | 
						|
        return unless optimized
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |