alns 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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/lib/alns/accept/base.rb +11 -0
- data/lib/alns/accept/hill_climbing.rb +13 -0
- data/lib/alns/math.rb +20 -0
- data/lib/alns/outcome.rb +21 -0
- data/lib/alns/random/random.rb +17 -0
- data/lib/alns/result.rb +5 -0
- data/lib/alns/select/base.rb +48 -0
- data/lib/alns/select/roulette_wheel.rb +51 -0
- data/lib/alns/solver.rb +96 -0
- data/lib/alns/state.rb +9 -0
- data/lib/alns/statistics.rb +33 -0
- data/lib/alns/stop/base.rb +11 -0
- data/lib/alns/stop/max_iterations.rb +20 -0
- data/lib/alns/stop/max_runtime.rb +24 -0
- data/lib/alns.rb +3 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0d806c11f4437a5a793860c605ebd32c5b85f266d66ee86d575735cb73306c25
|
|
4
|
+
data.tar.gz: 2e36605d0d6827b77681ca4a94f1b279c8e2d94f7857137c793c581fcaef4c8c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 57fd77798edad151c997b1e60c137a0ceabe031bba9a0d48dbf7f9b3e9f6c370a1d2bbaf8a5f203cb830dc2c3a557f9678a94d961b743d690b868a9a8af7adc2
|
|
7
|
+
data.tar.gz: 6979b6066219dbc028d4863e416e7892675fd88dfcbc9a9871d0bff2ec4f88a7577d750cce1305566c85651729fc3e41c20394ab970bb43035c296c443beefd5
|
data/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Niels Wouda and contributors
|
|
4
|
+
Copyright (c) 2025 bibenga
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
data/lib/alns/math.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ALNS
|
|
4
|
+
def self.weighted_random_index(rnd, weights)
|
|
5
|
+
raise 'Invalid weights: Array is empty' if weights.empty?
|
|
6
|
+
return 0 if weights.size == 1
|
|
7
|
+
|
|
8
|
+
total_sum = weights.sum
|
|
9
|
+
adjusted_value = rnd.rand * total_sum
|
|
10
|
+
|
|
11
|
+
weights.each_with_index do |weight, index|
|
|
12
|
+
adjusted_value -= weight
|
|
13
|
+
|
|
14
|
+
return index if adjusted_value <= 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# we will only be here when errors accumulate
|
|
18
|
+
weights.length - 1
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/alns/outcome.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ALNS
|
|
4
|
+
module Outcome
|
|
5
|
+
BEST = 0
|
|
6
|
+
BETTER = 1
|
|
7
|
+
ACCEPT = 2
|
|
8
|
+
REJECT = 3
|
|
9
|
+
|
|
10
|
+
def self.to_s(value)
|
|
11
|
+
case value
|
|
12
|
+
when BEST then 'BEST'
|
|
13
|
+
when BETTER then 'BETTER'
|
|
14
|
+
when ACCEPT then 'ACCEPT'
|
|
15
|
+
when REJECT then 'REJECT'
|
|
16
|
+
else
|
|
17
|
+
raise ArgumentError, "Invalid outcome: #{value}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module ALNS
|
|
6
|
+
module Random
|
|
7
|
+
class Random
|
|
8
|
+
def rand(max = nil)
|
|
9
|
+
if max.nil?
|
|
10
|
+
SecureRandom.random_number
|
|
11
|
+
else
|
|
12
|
+
SecureRandom.random_number(max)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/alns/result.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ALNS
|
|
4
|
+
module Select
|
|
5
|
+
class Base
|
|
6
|
+
def initialize(num_destroy, num_repair, op_coupling = nil)
|
|
7
|
+
if num_destroy <= 0 || num_repair <= 0
|
|
8
|
+
raise ArgumentError, 'Missing destroy or repair operators.'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
unless op_coupling.nil?
|
|
12
|
+
rows = op_coupling.length
|
|
13
|
+
cols = op_coupling[0].length
|
|
14
|
+
|
|
15
|
+
if rows != num_destroy || cols != num_repair
|
|
16
|
+
raise ArgumentError,
|
|
17
|
+
"coupling matrix of shape (#{rows}, #{cols}), expected (#{num_destroy}, #{num_repair})"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
op_coupling.each_with_index do |row, i|
|
|
21
|
+
if row.length != cols
|
|
22
|
+
raise ArgumentError,
|
|
23
|
+
"the number of columns in a row #{i} does not match the expected #{cols}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
coupled = row.any? { |b| b }
|
|
27
|
+
unless coupled
|
|
28
|
+
raise ArgumentError,
|
|
29
|
+
"destroy operator #{i} has no coupled repair operators"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@num_destroy = num_destroy
|
|
35
|
+
@num_repair = num_repair
|
|
36
|
+
@op_coupling = op_coupling
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def select(rnd, best, current)
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def update(candidate, d_idx, r_idx, outcome)
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'alns/select/base'
|
|
4
|
+
require 'alns/math'
|
|
5
|
+
|
|
6
|
+
module ALNS
|
|
7
|
+
module Select
|
|
8
|
+
class RouletteWheel < Base
|
|
9
|
+
def initialize(scores, decay, num_destroy, num_repair, op_coupling = nil)
|
|
10
|
+
super(num_destroy, num_repair, op_coupling)
|
|
11
|
+
|
|
12
|
+
@scores = scores
|
|
13
|
+
@decay = decay
|
|
14
|
+
|
|
15
|
+
@d_weights = Array.new(num_destroy, 1)
|
|
16
|
+
@r_weights = Array.new(num_repair, 1)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def select(rnd, _best, _current)
|
|
20
|
+
if @op_coupling
|
|
21
|
+
d_idx = ALNS.weighted_random_index(rnd, @d_weights)
|
|
22
|
+
|
|
23
|
+
coupled_r_idcs = []
|
|
24
|
+
coupled_r_weights = []
|
|
25
|
+
@op_coupling[d_idx].each_with_index do |coupled, i|
|
|
26
|
+
if coupled
|
|
27
|
+
coupled_r_idcs << i
|
|
28
|
+
coupled_r_weights << @r_weights[i]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
r_idx = coupled_r_idcs[ALNS.weighted_random_index(rnd, coupled_r_weights)]
|
|
33
|
+
|
|
34
|
+
[d_idx, r_idx]
|
|
35
|
+
else
|
|
36
|
+
d_idx = ALNS.weighted_random_index(rnd, @d_weights)
|
|
37
|
+
r_idx = ALNS.weighted_random_index(rnd, @r_weights)
|
|
38
|
+
[d_idx, r_idx]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def update(_candidate, d_idx, r_idx, outcome)
|
|
43
|
+
@d_weights[d_idx] *= @decay
|
|
44
|
+
@d_weights[d_idx] += (1 - @decay) * @scores[outcome]
|
|
45
|
+
|
|
46
|
+
@r_weights[r_idx] *= @decay
|
|
47
|
+
@r_weights[r_idx] += (1 - @decay) * @scores[outcome]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/alns/solver.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'alns/outcome'
|
|
4
|
+
require 'alns/statistics'
|
|
5
|
+
require 'alns/result'
|
|
6
|
+
|
|
7
|
+
module ALNS
|
|
8
|
+
# Implements the adaptive large neighbourhood search (ALNS) algorithm.
|
|
9
|
+
class Solver
|
|
10
|
+
attr_reader :rnd, :destroy_operators, :repair_operators
|
|
11
|
+
|
|
12
|
+
def initialize(rnd = nil)
|
|
13
|
+
@rnd = rnd || Random.new
|
|
14
|
+
@on_outcome = nil
|
|
15
|
+
@destroy_operators = []
|
|
16
|
+
@repair_operators = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_outcome(callback = nil, &block)
|
|
20
|
+
@on_outcome = callback || block
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_destroy_operator(callback = nil, &block)
|
|
24
|
+
@destroy_operators << (callback || block)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_repair_operator(callback = nil, &block)
|
|
28
|
+
@repair_operators << (callback || block)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def iterate(initial_solution, select, accept, stop)
|
|
32
|
+
if @destroy_operators.empty? || @repair_operators.empty?
|
|
33
|
+
raise ArgumentError, 'Missing destroy or repair operators.'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
curr = initial_solution
|
|
37
|
+
best = initial_solution
|
|
38
|
+
|
|
39
|
+
stats = Statistics.new(@destroy_operators.length, @repair_operators.length)
|
|
40
|
+
|
|
41
|
+
stats.collect_objective(Time.new, initial_solution.objective)
|
|
42
|
+
|
|
43
|
+
until stop.done?(@rnd, best, curr)
|
|
44
|
+
d_idx, r_idx = select.select(@rnd, best, curr)
|
|
45
|
+
|
|
46
|
+
destroy_op = @destroy_operators[d_idx]
|
|
47
|
+
repair_op = @repair_operators[r_idx]
|
|
48
|
+
|
|
49
|
+
destroyed = destroy_op.call(curr, @rnd)
|
|
50
|
+
cand = repair_op.call(destroyed, @rnd)
|
|
51
|
+
|
|
52
|
+
best, curr, outcome = eval_cand(accept, best, curr, cand)
|
|
53
|
+
|
|
54
|
+
select.update(cand, d_idx, r_idx, outcome)
|
|
55
|
+
|
|
56
|
+
stats.collect_objective(Time.new, curr.objective)
|
|
57
|
+
stats.collect_operators(d_idx, r_idx, outcome)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
stats.freeze
|
|
61
|
+
|
|
62
|
+
Result.new(best, stats)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def eval_cand(accept, best, curr, cand)
|
|
68
|
+
outcome = determine_outcome(accept, best, curr, cand)
|
|
69
|
+
|
|
70
|
+
@on_outcome&.call(outcome, cand)
|
|
71
|
+
|
|
72
|
+
case outcome
|
|
73
|
+
when Outcome::BEST
|
|
74
|
+
[cand, cand, outcome]
|
|
75
|
+
when Outcome::REJECT
|
|
76
|
+
[best, curr, outcome, nil]
|
|
77
|
+
else
|
|
78
|
+
[best, cand, outcome, nil]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def determine_outcome(accept, best, curr, cand)
|
|
83
|
+
outcome = Outcome::REJECT
|
|
84
|
+
|
|
85
|
+
if accept.accept?(@rnd, best, curr, cand)
|
|
86
|
+
outcome = Outcome::ACCEPT
|
|
87
|
+
|
|
88
|
+
outcome = Outcome::BETTER if cand.objective < curr.objective
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
outcome = Outcome::BEST if cand.objective < best.objective
|
|
92
|
+
|
|
93
|
+
outcome
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/alns/state.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ALNS
|
|
4
|
+
class Statistics
|
|
5
|
+
attr_reader :runtimes, :objectives, :destroy_operator_counts, :repair_operator_counts
|
|
6
|
+
|
|
7
|
+
def initialize(num_destroy, num_repair)
|
|
8
|
+
@runtimes = []
|
|
9
|
+
@objectives = []
|
|
10
|
+
@destroy_operator_counts = Array.new(num_destroy) { [0, 0, 0, 0] }
|
|
11
|
+
@repair_operator_counts = Array.new(num_repair) { [0, 0, 0, 0] }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def collect_objective(time, objective)
|
|
15
|
+
@runtimes << time
|
|
16
|
+
@objectives << objective
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def collect_operators(d_idx, r_idx, outcome)
|
|
20
|
+
@destroy_operator_counts[d_idx][outcome] += 1
|
|
21
|
+
@repair_operator_counts[r_idx][outcome] += 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def freeze
|
|
25
|
+
@runtimes.freeze
|
|
26
|
+
@objectives.freeze
|
|
27
|
+
destroy_operator_counts.each(&:freeze)
|
|
28
|
+
@destroy_operator_counts.freeze
|
|
29
|
+
repair_operator_counts.each(&:freeze)
|
|
30
|
+
@repair_operator_counts.freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'alns/stop/base'
|
|
4
|
+
|
|
5
|
+
module ALNS
|
|
6
|
+
module Stop
|
|
7
|
+
class MaxIterations < Base
|
|
8
|
+
def initialize(max_iterations)
|
|
9
|
+
super()
|
|
10
|
+
@max_iterations = max_iterations
|
|
11
|
+
@current_iteration = 0
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def done?(_rnd, _best, _current)
|
|
15
|
+
@current_iteration += 1
|
|
16
|
+
@current_iteration > @max_iterations
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'alns/stop/base'
|
|
4
|
+
|
|
5
|
+
module ALNS
|
|
6
|
+
module Stop
|
|
7
|
+
class MaxRuntime < Base
|
|
8
|
+
def initialize(max_runtime)
|
|
9
|
+
super()
|
|
10
|
+
@max_runtime = max_runtime
|
|
11
|
+
@started = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def done?(_rnd, _best, _current)
|
|
15
|
+
if @started.nil?
|
|
16
|
+
@started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
17
|
+
return false
|
|
18
|
+
end
|
|
19
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started
|
|
20
|
+
elapsed > @max_runtime
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/alns.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: alns
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- bibenga
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: ''
|
|
13
|
+
email: bibenga@users.noreply.github.com
|
|
14
|
+
executables: []
|
|
15
|
+
extensions: []
|
|
16
|
+
extra_rdoc_files: []
|
|
17
|
+
files:
|
|
18
|
+
- LICENSE
|
|
19
|
+
- lib/alns.rb
|
|
20
|
+
- lib/alns/accept/base.rb
|
|
21
|
+
- lib/alns/accept/hill_climbing.rb
|
|
22
|
+
- lib/alns/math.rb
|
|
23
|
+
- lib/alns/outcome.rb
|
|
24
|
+
- lib/alns/random/random.rb
|
|
25
|
+
- lib/alns/result.rb
|
|
26
|
+
- lib/alns/select/base.rb
|
|
27
|
+
- lib/alns/select/roulette_wheel.rb
|
|
28
|
+
- lib/alns/solver.rb
|
|
29
|
+
- lib/alns/state.rb
|
|
30
|
+
- lib/alns/statistics.rb
|
|
31
|
+
- lib/alns/stop/base.rb
|
|
32
|
+
- lib/alns/stop/max_iterations.rb
|
|
33
|
+
- lib/alns/stop/max_runtime.rb
|
|
34
|
+
homepage: https://github.com/bibenga/alns-ruby
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata:
|
|
38
|
+
source_code_uri: https://github.com/bibenga/alns-ruby
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.4'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.6.9
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Adaptive Large Neighbourhood Search
|
|
56
|
+
test_files: []
|