annealing 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +45 -0
- data/.rubocop.yml +19 -12
- data/.rubocop_todo.yml +42 -8
- data/CHANGELOG.md +25 -0
- data/Gemfile +8 -3
- data/Gemfile.lock +49 -5
- data/LICENSE +1 -1
- data/README.md +208 -36
- data/Rakefile +17 -6
- data/annealing.gemspec +17 -14
- data/bin/console +3 -3
- data/bin/run +57 -0
- data/lib/annealing/configuration/coolers.rb +36 -0
- data/lib/annealing/configuration/terminators.rb +25 -0
- data/lib/annealing/configuration.rb +64 -11
- data/lib/annealing/metal.rb +47 -0
- data/lib/annealing/simulator.rb +34 -20
- data/lib/annealing/version.rb +1 -1
- data/lib/annealing.rb +9 -15
- metadata +37 -9
- data/.travis.yml +0 -6
- data/lib/annealing/pool.rb +0 -55
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5435b7e34667fdfd63782d895080435f7f2dffb685b2a918bba83bf6781f432
|
4
|
+
data.tar.gz: 82bfb71e31afed6d53e0649b35c373fc5ea27bcc19e6b72ae8522983df1d4734
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54dd58f3c4c2007aa0c6cd0ca264dc9a53ddb9680a354ea2808ff3c7a8ebcae5fc032aefb70f9cdbb6fe023cd23d0dff014254c99a577e2119172597cefd5202
|
7
|
+
data.tar.gz: 21c78d1a11bc7ddf2e9b02e916f3bb94986944ad37ceb08cddf5a70178b82477be414e8478d1d51f26220a147f8442f5594667152bce496b7b98897204ec221f
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: Ruby
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: [ main ]
|
13
|
+
pull_request:
|
14
|
+
branches: [ main ]
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
lint:
|
18
|
+
runs-on: ubuntu-latest
|
19
|
+
strategy:
|
20
|
+
matrix:
|
21
|
+
ruby-version: ['3.0']
|
22
|
+
steps:
|
23
|
+
- uses: actions/checkout@v2
|
24
|
+
- name: Set up Ruby
|
25
|
+
uses: ruby/setup-ruby@v1
|
26
|
+
with:
|
27
|
+
ruby-version: ${{ matrix.ruby-version }}
|
28
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
29
|
+
- name: Run linter
|
30
|
+
run: bundle exec rubocop --parallel
|
31
|
+
test:
|
32
|
+
runs-on: ubuntu-latest
|
33
|
+
strategy:
|
34
|
+
matrix:
|
35
|
+
ruby-version: ['3.0', '3.1', '3.2']
|
36
|
+
|
37
|
+
steps:
|
38
|
+
- uses: actions/checkout@v2
|
39
|
+
- name: Set up Ruby
|
40
|
+
uses: ruby/setup-ruby@v1
|
41
|
+
with:
|
42
|
+
ruby-version: ${{ matrix.ruby-version }}
|
43
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
44
|
+
- name: Run tests
|
45
|
+
run: bundle exec rake test
|
data/.rubocop.yml
CHANGED
@@ -1,18 +1,25 @@
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
require:
|
4
|
+
- rubocop-performance
|
5
|
+
- rubocop-minitest
|
6
|
+
- rubocop-rake
|
7
|
+
|
8
|
+
AllCops:
|
9
|
+
TargetRubyVersion: 3.0
|
10
|
+
NewCops: enable
|
11
|
+
DefaultFormatter: progress
|
12
|
+
DisplayCopNames: true
|
13
|
+
DisplayStyleGuide: true
|
14
|
+
ExtraDetails: true
|
15
|
+
|
7
16
|
Layout/LineLength:
|
8
17
|
Max: 120
|
9
|
-
|
10
|
-
|
11
|
-
Lint/StructNewOverride:
|
12
|
-
Enabled: false
|
13
|
-
Style/HashEachMethods:
|
14
|
-
Enabled: true
|
15
|
-
Style/HashTransformKeys:
|
18
|
+
|
19
|
+
Style/StringLiterals:
|
16
20
|
Enabled: true
|
17
|
-
|
21
|
+
EnforcedStyle: double_quotes
|
22
|
+
|
23
|
+
Style/StringLiteralsInInterpolation:
|
18
24
|
Enabled: true
|
25
|
+
EnforcedStyle: double_quotes
|
data/.rubocop_todo.yml
CHANGED
@@ -1,20 +1,54 @@
|
|
1
1
|
# This configuration was generated by
|
2
|
-
# `rubocop --auto-gen-config`
|
3
|
-
# on
|
2
|
+
# `rubocop --auto-gen-config --auto-gen-only-exclude`
|
3
|
+
# on 2022-12-12 19:04:19 UTC using RuboCop version 1.23.0.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
8
|
|
9
|
-
# Offense count:
|
10
|
-
# Configuration parameters: IgnoredMethods.
|
9
|
+
# Offense count: 7
|
10
|
+
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes, Max.
|
11
|
+
Metrics/AbcSize:
|
12
|
+
Exclude:
|
13
|
+
- 'lib/annealing/configuration.rb'
|
14
|
+
- 'test/annealing/configuration_test.rb'
|
15
|
+
- 'test/annealing/metal_test.rb'
|
16
|
+
- 'test/annealing/simulator_test.rb'
|
17
|
+
- 'test/annealing_test.rb'
|
11
18
|
|
19
|
+
# Offense count: 2
|
20
|
+
# Configuration parameters: CountComments, Max, CountAsOne.
|
21
|
+
Metrics/ClassLength:
|
22
|
+
Exclude:
|
23
|
+
- 'test/annealing/configuration_test.rb'
|
24
|
+
- 'test/annealing/simulator_test.rb'
|
12
25
|
|
13
26
|
# Offense count: 1
|
14
|
-
# Configuration parameters:
|
27
|
+
# Configuration parameters: IgnoredMethods, Max.
|
28
|
+
Metrics/CyclomaticComplexity:
|
29
|
+
Exclude:
|
30
|
+
- 'lib/annealing/configuration.rb'
|
15
31
|
|
32
|
+
# Offense count: 11
|
33
|
+
# Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
|
34
|
+
Metrics/MethodLength:
|
35
|
+
Exclude:
|
36
|
+
- 'lib/annealing/configuration.rb'
|
37
|
+
- 'test/annealing/configuration_test.rb'
|
38
|
+
- 'test/annealing/metal_test.rb'
|
39
|
+
- 'test/annealing/simulator_test.rb'
|
40
|
+
- 'test/annealing_test.rb'
|
16
41
|
|
17
|
-
# Offense count:
|
18
|
-
|
42
|
+
# Offense count: 1
|
43
|
+
# Configuration parameters: IgnoredMethods, Max.
|
44
|
+
Metrics/PerceivedComplexity:
|
45
|
+
Exclude:
|
46
|
+
- 'lib/annealing/configuration.rb'
|
47
|
+
|
48
|
+
# Offense count: 5
|
49
|
+
# Configuration parameters: Max.
|
50
|
+
Minitest/MultipleAssertions:
|
19
51
|
Exclude:
|
20
|
-
- 'test
|
52
|
+
- 'test/annealing/configuration/terminators_test.rb'
|
53
|
+
- 'test/annealing/configuration_test.rb'
|
54
|
+
- 'test/annealing/simulator_test.rb'
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
|
4
|
+
## [0.3.0] - 2022-02-21
|
5
|
+
|
6
|
+
- Removed the default state change function; you are now required to define a custom `state_change` function for the simulation (#2)
|
7
|
+
- Added support for specifying a custom `termination_condition` function to override the default condition of the temperature reaching 0 (#5)
|
8
|
+
- Added support for specifying a custom `cool_down` function to override the default linear cooling function (#9)
|
9
|
+
- Normalized configurations options such that they can be specified in a consistent way across many interfaces (#19)
|
10
|
+
- `Annealing::Pool` has been replaced with `Annealing::Metal` which has a different interface from the old class
|
11
|
+
- `Annealing.simulate`, `Annealing::Simulator.new` and `Annealing::Simulator#run` method signatures have changed to accommodate normalized configuration options
|
12
|
+
- `Annealing::Simulator.new` no longer raises `RuntimeError` exceptions if configuration options are invalid. Instead, they will be raised from `Annealing::Simulator#run` as `ArgumentError` exceptions.
|
13
|
+
- Negative `cooling_rate` values are no longer valid; `Annealing::Simulator#run` will raise an error if one is specified
|
14
|
+
- Comprehensive test suite added
|
15
|
+
|
16
|
+
## [0.2.0] - 2020-07-23
|
17
|
+
|
18
|
+
- Improve private methods
|
19
|
+
- Minor code cleanup
|
20
|
+
|
21
|
+
## [0.1.0] - 2020-07-17
|
22
|
+
|
23
|
+
- It allows custom configuration
|
24
|
+
- It use distance method to calculate energy
|
25
|
+
- It allows user to implement their own total energy calculator
|
data/Gemfile
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source "https://rubygems.org"
|
4
4
|
|
5
5
|
# Specify your gem's dependencies in annealing.gemspec
|
6
6
|
gemspec
|
7
7
|
|
8
|
-
gem
|
9
|
-
gem
|
8
|
+
gem "debug", ">= 1.0.0", require: false
|
9
|
+
gem "minitest", "~> 5.0"
|
10
|
+
gem "rake", "~> 13.0.0", ">= 13.0.6"
|
11
|
+
gem "rubocop", "~> 1.48.0"
|
12
|
+
gem "rubocop-minitest", "~> 0.29.0"
|
13
|
+
gem "rubocop-performance", "~> 1.16.0"
|
14
|
+
gem "rubocop-rake", "~> 0.6.0"
|
data/Gemfile.lock
CHANGED
@@ -1,21 +1,65 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
annealing (0.
|
4
|
+
annealing (0.4.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
|
-
|
10
|
-
|
9
|
+
ast (2.4.2)
|
10
|
+
debug (1.4.0)
|
11
|
+
irb (>= 1.3.6)
|
12
|
+
reline (>= 0.2.7)
|
13
|
+
io-console (0.6.0)
|
14
|
+
irb (1.6.3)
|
15
|
+
reline (>= 0.3.0)
|
16
|
+
json (2.6.3)
|
17
|
+
minitest (5.18.0)
|
18
|
+
parallel (1.22.1)
|
19
|
+
parser (3.2.1.1)
|
20
|
+
ast (~> 2.4.1)
|
21
|
+
rainbow (3.1.1)
|
22
|
+
rake (13.0.6)
|
23
|
+
regexp_parser (2.7.0)
|
24
|
+
reline (0.3.2)
|
25
|
+
io-console (~> 0.5)
|
26
|
+
rexml (3.2.5)
|
27
|
+
rubocop (1.48.0)
|
28
|
+
json (~> 2.3)
|
29
|
+
parallel (~> 1.10)
|
30
|
+
parser (>= 3.2.0.0)
|
31
|
+
rainbow (>= 2.2.2, < 4.0)
|
32
|
+
regexp_parser (>= 1.8, < 3.0)
|
33
|
+
rexml (>= 3.2.5, < 4.0)
|
34
|
+
rubocop-ast (>= 1.26.0, < 2.0)
|
35
|
+
ruby-progressbar (~> 1.7)
|
36
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
37
|
+
rubocop-ast (1.27.0)
|
38
|
+
parser (>= 3.2.1.0)
|
39
|
+
rubocop-minitest (0.29.0)
|
40
|
+
rubocop (>= 1.39, < 2.0)
|
41
|
+
rubocop-performance (1.16.0)
|
42
|
+
rubocop (>= 1.7.0, < 2.0)
|
43
|
+
rubocop-ast (>= 0.4.0)
|
44
|
+
rubocop-rake (0.6.0)
|
45
|
+
rubocop (~> 1.0)
|
46
|
+
ruby-prof (1.6.1)
|
47
|
+
ruby-progressbar (1.13.0)
|
48
|
+
unicode-display_width (2.4.2)
|
11
49
|
|
12
50
|
PLATFORMS
|
13
51
|
ruby
|
14
52
|
|
15
53
|
DEPENDENCIES
|
16
54
|
annealing!
|
55
|
+
debug (>= 1.0.0)
|
17
56
|
minitest (~> 5.0)
|
18
|
-
rake (~>
|
57
|
+
rake (~> 13.0.0, >= 13.0.6)
|
58
|
+
rubocop (~> 1.48.0)
|
59
|
+
rubocop-minitest (~> 0.29.0)
|
60
|
+
rubocop-performance (~> 1.16.0)
|
61
|
+
rubocop-rake (~> 0.6.0)
|
62
|
+
ruby-prof (~> 1.6, >= 1.6.1)
|
19
63
|
|
20
64
|
BUNDLED WITH
|
21
|
-
2.
|
65
|
+
2.4.4
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# Annealing
|
2
|
+
|
2
3
|
[![Gem Version](https://badge.fury.io/rb/annealing.svg)](https://badge.fury.io/rb/annealing)
|
4
|
+
[![Ruby](https://github.com/3zcurdia/annealing/actions/workflows/ruby.yml/badge.svg)](https://github.com/3zcurdia/annealing/actions/workflows/ruby.yml)
|
3
5
|
|
4
|
-
Find the optimal solution in a complex problem through a simulated annealing implementation for
|
6
|
+
Find the optimal solution in a complex problem through a simulated annealing implementation for Ruby objects.
|
5
7
|
|
6
8
|
## Installation
|
7
9
|
|
@@ -13,18 +15,27 @@ gem 'annealing'
|
|
13
15
|
|
14
16
|
And then execute:
|
15
17
|
|
16
|
-
|
18
|
+
```shell
|
19
|
+
bundle install
|
20
|
+
```
|
17
21
|
|
18
22
|
Or install it yourself as:
|
19
23
|
|
20
|
-
|
24
|
+
```shell
|
25
|
+
gem install annealing
|
26
|
+
```
|
21
27
|
|
22
28
|
## Usage
|
23
29
|
|
24
|
-
|
30
|
+
Simulated annealing algorithms work by comparing multiple permutations of a given object and measuring their relative efficiencies based on any number of competing factors. If you aren't already familiar with the concept of simulated annealing, we recommend watching [The Most Metal Algorithm in Computer Science](https://www.youtube.com/watch?v=I_0GBWCKft8) from [SciShow](https://www.youtube.com/c/SciShow) as it will help you understand some of the concepts and terms used below.
|
31
|
+
|
32
|
+
In order to use this algorithm we must first define 3 things:
|
33
|
+
|
34
|
+
1. an initial object state to evaluate
|
35
|
+
2. a way to measure the energy of that state
|
36
|
+
3. and a way to change the state of the object over time
|
25
37
|
|
26
|
-
Lets
|
27
|
-
to mesure the distance between two locations.
|
38
|
+
Lets use the the traveling salesperson problem as an example. First we will define a Location object with a `distance` method to measure the distance between two locations.
|
28
39
|
|
29
40
|
```ruby
|
30
41
|
Location = Struct.new(:x, :y) do
|
@@ -40,7 +51,7 @@ Location = Struct.new(:x, :y) do
|
|
40
51
|
end
|
41
52
|
```
|
42
53
|
|
43
|
-
Now we can create an array of the
|
54
|
+
Now we can create an array of locations the salesperson will visit. This is our initial state, and the order can be any random starting state.
|
44
55
|
|
45
56
|
```ruby
|
46
57
|
locations = [
|
@@ -49,69 +60,230 @@ locations = [
|
|
49
60
|
Location.new(40, 120),
|
50
61
|
Location.new(100, 120),
|
51
62
|
Location.new(20, 40)
|
52
|
-
]
|
63
|
+
].shuffle
|
64
|
+
```
|
65
|
+
|
66
|
+
Next we need a way to calculate the total energy of traveling to each location in turn, with low energy states preferable to high energy states. Think of it as a representation of the efficiency of the trip; the further away one point is away from the next, the less efficient the trip is.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
energy_calculator = lambda do |locations|
|
70
|
+
locations.each_cons(2).sum do |location1, location2|
|
71
|
+
location1.distance(location2)
|
72
|
+
end
|
73
|
+
end
|
53
74
|
```
|
54
75
|
|
55
|
-
|
76
|
+
Finally, we need a way to make small, random changes in the order of locations the salesperson will visit as we probe for an optimal route.
|
56
77
|
|
57
78
|
```ruby
|
58
|
-
|
59
|
-
|
79
|
+
state_change = lambda do |locations|
|
80
|
+
size = locations.size
|
81
|
+
swapped = locations.dup
|
82
|
+
idx_a = rand(size)
|
83
|
+
idx_b = rand(size)
|
84
|
+
swapped[idx_b], swapped[idx_a] = swapped[idx_a], swapped[idx_b]
|
85
|
+
swapped
|
86
|
+
end
|
60
87
|
```
|
61
88
|
|
62
|
-
|
63
|
-
But if you want to iterate with other parameters you can send the parameters `temperature` and `cooling_rate`.
|
89
|
+
Now we can run the simulation. With the default configuration it will consider ~33 million permutations of the route, so this may take several minutes to complete.
|
64
90
|
|
65
91
|
```ruby
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
[(20,40), (40,120), (100,120), (60,200), (180,200)]
|
92
|
+
optimal_route = Annealing.simulate(locations,
|
93
|
+
energy_calculator: energy_calculator,
|
94
|
+
state_change: state_change)
|
95
|
+
optimal_route.state
|
96
|
+
# => [(20,40), (40,120), (100,120), (60,200), (180,200)]
|
72
97
|
```
|
73
98
|
|
74
|
-
|
99
|
+
## Configuration options
|
100
|
+
|
101
|
+
The annealer supports a number of configuration options. See the [configuration precedence](#configuration-precedence) section below for information on the different scopes they can be applied to.
|
102
|
+
|
103
|
+
### `cool_down`
|
104
|
+
|
105
|
+
By default, the simulation will decrease the `temperature` linearly by `cooling_rate` on each step of the annealing process. In some cases you may wish to override this to use a different cooling algorithm. To do so, you can use one of the other built-in cooling functions or you can specify a custom `cool_down` function. Custom functions can be any object that responds to `#call` and accepts four arguments: the `energy` calculation of the current object, the current `temperature` of the annealer, the `cooling_rate` for the simulation, and the number of `steps` the annealer has taken so far. It should return the new temperature as a Float.
|
75
106
|
|
76
107
|
```ruby
|
77
|
-
|
78
|
-
|
79
|
-
|
108
|
+
# Use the built-in linear cool-down function (the default)
|
109
|
+
Annealing.configuration.cool_down = Annealing::Configuration::Coolers.linear
|
110
|
+
|
111
|
+
# Use the built-in exponential cool-down function
|
112
|
+
Annealing.configuration.cool_down = Annealing::Configuration::Coolers.exponential
|
113
|
+
|
114
|
+
# Use the built-in geometric cool-down function with a custom ratio (default ratio is 2)
|
115
|
+
Annealing.configuration.cool_down = Annealing::Configuration::Coolers.exponential(1.5)
|
116
|
+
|
117
|
+
# Use a custom cool down function
|
118
|
+
Annealing.configuration.cool_down = lambda do |energy, temperature, cooling_rate, steps|
|
119
|
+
# Reduce temperature exponentially when the temperature is above 500, then linearly
|
120
|
+
if temperature > 500
|
121
|
+
Annealing::Configuration::Coolers.exponential.call(energy, temperature, cooling_rate, steps)
|
122
|
+
else
|
123
|
+
Annealing::Configuration::Coolers.linear.call(energy, temperature, cooling_rate, steps)
|
124
|
+
end
|
80
125
|
end
|
81
126
|
```
|
82
127
|
|
83
|
-
###
|
128
|
+
### `cooling_rate` and `temperature`
|
84
129
|
|
85
|
-
|
130
|
+
In the default configuration, the `cooling_rate` represents the amount by which the `temperature` will be reduced at each step, such that `temperature / cooling_rate` equals the maximum number of steps the annealer will go through in its search for the optimal solution. If a custom `cool_down` function is specified then `cooling_rate` will be passed to that function at each step along with the current temperature. The default `cooling_rate` value is `0.0003` and the default `temperature` is `10_000`.
|
86
131
|
|
87
132
|
```ruby
|
88
|
-
Annealing.configure do |
|
89
|
-
|
90
|
-
|
133
|
+
Annealing.configure do |config|
|
134
|
+
config.cooling_rate = 0.001
|
135
|
+
config.temperature = 25_000
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
Generally speaking, simulations have a higher chance of finding optimal solutions with a high initial temperature and a low cooling rate. A high temperature gives the simulation more time to search through neighboring states for low energy configurations, while a low cooling rate increases the probability that the simulation will select a low-energy configuration when comparing two states. For example, consider two configurations: `temperature(10000) / cooling_rate(1)` and `temperature(100) / cooling_rate(0.01)`. Even though both provide 10,000 steps when using the default cool down function, the latter configuration will allow for smaller temperature readings which is more likely to result in a more optimal final state.
|
140
|
+
|
141
|
+
### `energy_calculator`
|
142
|
+
|
143
|
+
You must specify a `energy_calculator` function before running any simulations; no default function is provided. The function can be any object that responds to `#call` and accepts a single argument: the `state` representing the current state being measured. It should return a measurement representing the efficiency of the current state based on all of its competing factors, and where a lower value represents a better configuration than a higher value.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
# A custom calculator class that takes into account hypothetical external factors
|
147
|
+
class PotentialSalesCalculator
|
148
|
+
def initialize(initial_time_of_day)
|
149
|
+
@initial_time_of_day = initial_time_of_day
|
150
|
+
end
|
151
|
+
|
152
|
+
def energy(locations)
|
153
|
+
arrival_time = @initial_time_of_day
|
154
|
+
first_location_sales = potential_sales(locations.first, arrival_time)
|
155
|
+
locations.each_cons(2).sum do |location1, location2|
|
156
|
+
arrival_time += travel_time(location1, location2, arrival_time)
|
157
|
+
distance = location1.distance(location2)
|
158
|
+
potential_sales(locations.first, arrival_time) / distance
|
159
|
+
end + first_location_sales
|
160
|
+
end
|
161
|
+
|
162
|
+
def potential_sales(location, time_of_day)
|
163
|
+
habits = CustomerHabits.new(location)
|
164
|
+
customers = habits.whos_home_at(time_of_day)
|
165
|
+
SalesTrends.estimate(customers)
|
166
|
+
end
|
167
|
+
|
168
|
+
def travel_time(location1, location2, time_of_day)
|
169
|
+
traffic = TrafficPredictor.new(location1, location2)
|
170
|
+
traffic.travel_time(time_of_day)
|
91
171
|
end
|
92
172
|
end
|
173
|
+
|
174
|
+
calculator = PotentialSalesCalculator.new(8)
|
175
|
+
Annealing.configuration.energy_calculator = calculator.method(:energy)
|
93
176
|
```
|
94
177
|
|
95
|
-
|
178
|
+
### `state_change`
|
179
|
+
|
180
|
+
As with `energy_calculator`, you must specify a `state_change` function in order to run any simulations; no default function is provided. The function can be any object that responds to `#call` and accepts a single argument: the `state` representing the current state that should be changed. It should return the changed state.
|
96
181
|
|
97
182
|
```ruby
|
98
|
-
|
99
|
-
|
183
|
+
class MyClass
|
184
|
+
def state_change(state)
|
185
|
+
size = state.size
|
186
|
+
swapped = state.dup
|
187
|
+
idx_a = rand(size)
|
188
|
+
idx_b = rand(size)
|
189
|
+
swapped[idx_b], swapped[idx_a] = swapped[idx_a], swapped[idx_b]
|
190
|
+
swapped
|
191
|
+
end
|
100
192
|
end
|
101
|
-
|
102
|
-
|
193
|
+
|
194
|
+
instance = MyClass.new
|
195
|
+
Annealing.configuration.state_change = instance.method(:state_change)
|
196
|
+
```
|
197
|
+
|
198
|
+
### `termination_condition`
|
199
|
+
|
200
|
+
By default, a simulation will run until the temperature reaches 0. In some cases, you might want to specify a termination condition that will stop the annealing process as soon as some other condition is met regardless of the current temperature. To do so, you can use one of the other built-in termination condition functions or you can specify a custom one. Custom `termination_condition` functions can be any object that responds to `#call` and accepts three arguments: the current `state` of the object, the `energy` calculation of the current object, and the current `temperature` of the simulation. It should return a boolean value where `true` indicates the simulation should stop.
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
# Use the built-in zero-temperature termination condition function
|
204
|
+
Annealing.configuration.termination_condition = Annealing::Configuration::Terminators.temp_is_zero?
|
205
|
+
|
206
|
+
# Use the built-in zero-energy termination condition function
|
207
|
+
Annealing.configuration.termination_condition = Annealing::Configuration::Terminators.energy_or_temp_is_zero?
|
208
|
+
|
209
|
+
# Use a custom termination condition function
|
210
|
+
Annealing.configuration.termination_condition = lambda do |_state, energy, temperature|
|
211
|
+
# Stop if the energy is below 500 and the temperature is below 100, or the temperature is already 0
|
212
|
+
temperature <= 0 || (energy <= 500 && temperature <= 500)
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
## Configuration precedence
|
217
|
+
|
218
|
+
Configuration options can be set globally using `Annealing.configuration` or `Annealing.configure`, on `Annealing::Simulator.new` to be used on all subsequent runs of that instance, and just-in-time on `Annealing.simulate` and `Annealing::Simulator#run`. They are applied in reverse order of precedence.
|
219
|
+
|
220
|
+
### Global configuration
|
221
|
+
|
222
|
+
Global configuration options, including the defaults, have the lowest precedence. They will be used in every simulation when no overriding configuration options are present. For instance, we can rewrite the traveling salesperson example like so:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
# Set globally using block style
|
226
|
+
Annealing.configure do |config|
|
227
|
+
config.energy_calculator = energy_calculator
|
228
|
+
end
|
229
|
+
|
230
|
+
# Or set individually
|
231
|
+
Annealing.configuration.state_change = state_change
|
232
|
+
|
233
|
+
# Now we don't need to specify them just in time
|
234
|
+
solution = Annealing.simulate(locations)
|
235
|
+
```
|
236
|
+
|
237
|
+
### Instance configuration
|
238
|
+
|
239
|
+
Instance configurations can be set on new instances of `Annealing::Simulator` objects and will apply to all subsequent simulation runs for that instance. Instance configuration options override their global configuration counterparts.
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
Annealing.configure do |config|
|
243
|
+
config.energy_calculator = energy_calculator
|
244
|
+
config.state_change = state_change
|
245
|
+
config.temperature = 10_000
|
246
|
+
end
|
247
|
+
|
248
|
+
simulation = Annealing::Simulator.new(temperature: 1_000)
|
249
|
+
simulation.run(locations) # Will use an initial temperature value of 1000
|
250
|
+
simulation.run(locations.shuffle) # So will this
|
251
|
+
```
|
252
|
+
|
253
|
+
### Just-in-time configuration
|
254
|
+
|
255
|
+
Just-in-time configuration options have the highest precedence and will override both global and instance options. They are only applied to the current simulation run.
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
Annealing.configure do |config|
|
259
|
+
config.cooling_rate = 0.001
|
260
|
+
config.energy_calculator = energy_calculator
|
261
|
+
config.state_change = state_change
|
262
|
+
config.temperature = 10_000
|
263
|
+
end
|
264
|
+
|
265
|
+
# Will use an initial temperature of 20,000 and a cooling rate of 0.001
|
266
|
+
solution = Annealing.simulate(locations, temperature: 20_000)
|
267
|
+
|
268
|
+
# Set an instance cooling rate of 0.002
|
269
|
+
simulation = Annealing::Simulator.new(cooling_rate: 0.002)
|
270
|
+
|
271
|
+
# Will use an initial temperature of 20,000 and a cooling rate of 0.002
|
272
|
+
simulation.run(locations, temperature: 20_000)
|
273
|
+
|
274
|
+
# Will use an initial temperature of 10,000 and a cooling rate of 0.003
|
275
|
+
simulation.run(locations.shuffle, cooling_rate: 0.003)
|
103
276
|
```
|
104
277
|
|
105
278
|
## Development
|
106
279
|
|
107
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
|
280
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the test suite. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
108
281
|
|
109
282
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
110
283
|
|
111
284
|
## Contributing
|
112
285
|
|
113
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/3zcurdia/annealing. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/3zcurdia/annealing/blob/master/CODE_OF_CONDUCT.md).
|
114
|
-
|
286
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/3zcurdia/annealing](https://github.com/3zcurdia/annealing). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/3zcurdia/annealing/blob/master/CODE_OF_CONDUCT.md).
|
115
287
|
|
116
288
|
## Code of Conduct
|
117
289
|
|
data/Rakefile
CHANGED
@@ -1,12 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
require "rubocop/rake_task"
|
5
6
|
|
6
7
|
Rake::TestTask.new(:test) do |t|
|
7
|
-
t.libs <<
|
8
|
-
t.libs <<
|
9
|
-
t.test_files = FileList[
|
8
|
+
t.libs << "test"
|
9
|
+
t.libs << "lib"
|
10
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
11
|
end
|
11
12
|
|
12
|
-
|
13
|
+
RuboCop::RakeTask.new(:rubocop)
|
14
|
+
|
15
|
+
desc "Execute the end-to-end integration test script"
|
16
|
+
task :end_to_end_test do
|
17
|
+
puts
|
18
|
+
puts "Running full end-to-end test with bin/run"
|
19
|
+
puts `bin/run`
|
20
|
+
puts
|
21
|
+
end
|
22
|
+
|
23
|
+
task default: %i[test end_to_end_test rubocop]
|
data/annealing.gemspec
CHANGED
@@ -1,31 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "lib/annealing/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name =
|
6
|
+
spec.name = "annealing"
|
7
7
|
spec.version = Annealing::VERSION
|
8
|
-
spec.authors = [
|
9
|
-
spec.email = [
|
10
|
-
spec.license =
|
8
|
+
spec.authors = ["Luis Ezcurdia", "Chris Bloom"]
|
9
|
+
spec.email = ["ing.ezcurdia@gmail.com", "chrisbloom7@gmail.com"]
|
10
|
+
spec.license = "MIT"
|
11
11
|
|
12
|
-
spec.summary =
|
13
|
-
spec.description =
|
14
|
-
spec.homepage =
|
15
|
-
spec.required_ruby_version = Gem::Requirement.new(
|
12
|
+
spec.summary = "Simulated Annealing"
|
13
|
+
spec.description = "Simulated Annealing algoritm implementation."
|
14
|
+
spec.homepage = "https://github.com/3zcurdia/annealing"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
16
16
|
|
17
17
|
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
18
18
|
|
19
|
-
spec.metadata[
|
20
|
-
spec.metadata[
|
21
|
-
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/3zcurdia/annealing"
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/3zcurdia/annealing/blob/main/CHANGELOG.md"
|
22
22
|
|
23
23
|
# Specify which files should be added to the gem when it is released.
|
24
24
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
25
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
26
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
27
|
end
|
28
|
-
spec.bindir =
|
28
|
+
spec.bindir = "exe"
|
29
29
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
-
spec.require_paths = [
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_development_dependency "ruby-prof", "~> 1.6", ">= 1.6.1"
|
33
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
31
34
|
end
|
data/bin/console
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require "bundler/setup"
|
5
|
+
require "annealing"
|
6
6
|
|
7
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -11,5 +11,5 @@ require 'annealing'
|
|
11
11
|
# require "pry"
|
12
12
|
# Pry.start
|
13
13
|
|
14
|
-
require
|
14
|
+
require "irb"
|
15
15
|
IRB.start(__FILE__)
|
data/bin/run
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "annealing"
|
6
|
+
|
7
|
+
Location = Struct.new(:name, :x, :y) do
|
8
|
+
def inspect
|
9
|
+
"#{name} (#{x},#{y})"
|
10
|
+
end
|
11
|
+
|
12
|
+
def distance(location)
|
13
|
+
dx = (x - location.x).abs
|
14
|
+
dy = (y - location.y).abs
|
15
|
+
Math.sqrt((dx**2) + (dy**2))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
locations = [
|
20
|
+
Location.new("Blaire Hills", 60, 200),
|
21
|
+
Location.new("Smallville", 180, 200),
|
22
|
+
Location.new("Boggs Harbor", 40, 120),
|
23
|
+
Location.new("Curtisville", 100, 120),
|
24
|
+
Location.new("Allentown", 20, 40)
|
25
|
+
].shuffle
|
26
|
+
|
27
|
+
energy_calculator = lambda do |state|
|
28
|
+
state.each_cons(2).sum do |location1, location2|
|
29
|
+
location1.distance(location2)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
state_change = lambda do |state|
|
34
|
+
size = state.size
|
35
|
+
swapped = state.dup
|
36
|
+
idx_a = rand(size)
|
37
|
+
idx_b = rand(size)
|
38
|
+
swapped[idx_b], swapped[idx_a] = swapped[idx_a], swapped[idx_b]
|
39
|
+
swapped
|
40
|
+
end
|
41
|
+
|
42
|
+
simulator = Annealing::Simulator.new(temperature: 10_000, cooling_rate: 0.01)
|
43
|
+
solution = simulator.run(locations,
|
44
|
+
energy_calculator: energy_calculator,
|
45
|
+
state_change: state_change)
|
46
|
+
|
47
|
+
puts "\nInitial itinerary:"
|
48
|
+
locations.each_cons(2).with_index do |(location1, location2), index|
|
49
|
+
puts "Stop ##{index + 1}: #{location1.name} -> #{location2.name} (#{location1.distance(location2)})"
|
50
|
+
end
|
51
|
+
puts "-------\nEnergy: #{energy_calculator.call(locations)}"
|
52
|
+
|
53
|
+
puts "\nAnnealed itinerary:"
|
54
|
+
solution.state.each_cons(2).with_index do |(location1, location2), index|
|
55
|
+
puts "Stop ##{index + 1}: #{location1.name} -> #{location2.name} (#{location1.distance(location2)})"
|
56
|
+
end
|
57
|
+
puts "-------\nEnergy: #{solution.energy}"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Annealing
|
4
|
+
class Configuration
|
5
|
+
# Built-in cool down functions
|
6
|
+
module Coolers
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Reduce temperature linearly by the cooling rate
|
10
|
+
def linear
|
11
|
+
lambda { |_energy, temperature, cooling_rate, _steps|
|
12
|
+
temperature - cooling_rate
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Reduce temperature exponentially on each step by the cooling rate
|
17
|
+
def exponential
|
18
|
+
lambda { |_energy, temperature, cooling_rate, steps|
|
19
|
+
temperature - (Math.exp(steps - 1) * cooling_rate)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Reduce temperature geometrically at a given ratio by the cooling rate
|
24
|
+
def geometric(ratio = 2)
|
25
|
+
unless ratio.positive?
|
26
|
+
raise(Annealing::Configuration::ConfigurationError,
|
27
|
+
"geometric ratio must be positive")
|
28
|
+
end
|
29
|
+
|
30
|
+
lambda { |_energy, temperature, cooling_rate, steps|
|
31
|
+
temperature - (cooling_rate * (ratio**(steps - 1)))
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Annealing
|
4
|
+
class Configuration
|
5
|
+
# Built-in termination condition check functions
|
6
|
+
module Terminators
|
7
|
+
module_function
|
8
|
+
|
9
|
+
# Returns true when the temperature is at or below zero
|
10
|
+
def temp_is_zero?
|
11
|
+
lambda { |_state, _energy, temperature|
|
12
|
+
temperature <= 0
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns true if a 0-energy state is detected, or when the temperature
|
17
|
+
# is at or below zero
|
18
|
+
def energy_or_temp_is_zero?
|
19
|
+
lambda { |state, energy, temperature|
|
20
|
+
energy <= 0 || temp_is_zero?.call(state, energy, temperature)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -3,17 +3,70 @@
|
|
3
3
|
module Annealing
|
4
4
|
# It enables the gem configuration
|
5
5
|
class Configuration
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
6
|
+
DEFAULT_COOLING_RATE = 0.0003
|
7
|
+
DEFAULT_INITIAL_TEMPERATURE = 10_000.0
|
8
|
+
|
9
|
+
class ConfigurationError < Annealing::Error; end
|
10
|
+
|
11
|
+
attr_accessor :cool_down,
|
12
|
+
:cooling_rate,
|
13
|
+
:energy_calculator,
|
14
|
+
:state_change,
|
15
|
+
:temperature,
|
16
|
+
:termination_condition
|
17
|
+
|
18
|
+
def initialize(config_hash = {})
|
19
|
+
@cool_down = config_hash.fetch(:cool_down, Coolers.linear)
|
20
|
+
@cooling_rate = config_hash.fetch(:cooling_rate,
|
21
|
+
DEFAULT_COOLING_RATE).to_f
|
22
|
+
@energy_calculator = config_hash.fetch(:energy_calculator, nil)
|
23
|
+
@state_change = config_hash.fetch(:state_change, nil)
|
24
|
+
@temperature = config_hash.fetch(:temperature,
|
25
|
+
DEFAULT_INITIAL_TEMPERATURE).to_f
|
26
|
+
@termination_condition = config_hash.fetch(:termination_condition,
|
27
|
+
Terminators.temp_is_zero?)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Return new configuration that merges new attributes with current
|
31
|
+
def merge(config_hash)
|
32
|
+
self.class.new(attributes.merge(config_hash))
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate!
|
36
|
+
message = if !callable?(cool_down)
|
37
|
+
"Missing cool down function"
|
38
|
+
elsif cooling_rate.negative?
|
39
|
+
"Cooling rate cannot be negative"
|
40
|
+
elsif !callable?(energy_calculator)
|
41
|
+
"Missing energy calculator function"
|
42
|
+
elsif !callable?(state_change)
|
43
|
+
"Missing state change function"
|
44
|
+
elsif temperature.negative?
|
45
|
+
"Initial temperature cannot be negative"
|
46
|
+
elsif !callable?(termination_condition)
|
47
|
+
"Missing termination condition function"
|
48
|
+
end
|
49
|
+
raise(ConfigurationError, message) if message
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def attributes
|
55
|
+
{
|
56
|
+
cool_down: cool_down,
|
57
|
+
cooling_rate: cooling_rate,
|
58
|
+
energy_calculator: energy_calculator,
|
59
|
+
state_change: state_change,
|
60
|
+
temperature: temperature,
|
61
|
+
termination_condition: termination_condition
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def callable?(attribute)
|
66
|
+
attribute.respond_to?(:call)
|
17
67
|
end
|
18
68
|
end
|
19
69
|
end
|
70
|
+
|
71
|
+
require "annealing/configuration/coolers"
|
72
|
+
require "annealing/configuration/terminators"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Annealing
|
4
|
+
# It manages the total energy of a given collection
|
5
|
+
class Metal
|
6
|
+
attr_reader :configuration, :state, :temperature
|
7
|
+
|
8
|
+
def initialize(current_state, current_temperature, configuration = nil)
|
9
|
+
@configuration = configuration || Annealing.configuration.merge({})
|
10
|
+
@state = current_state
|
11
|
+
@temperature = current_temperature
|
12
|
+
end
|
13
|
+
|
14
|
+
def energy
|
15
|
+
@energy ||= configuration.energy_calculator.call(state)
|
16
|
+
end
|
17
|
+
|
18
|
+
# This method is not idempotent!
|
19
|
+
# It relies on random probability to select the next state
|
20
|
+
def cool!(new_temperature)
|
21
|
+
cooled_metal = cool(new_temperature)
|
22
|
+
if prefer?(cooled_metal)
|
23
|
+
cooled_metal
|
24
|
+
else
|
25
|
+
@temperature = new_temperature
|
26
|
+
self
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# True if cooled_metal.energy is lower than current energy, otherwise let
|
33
|
+
# probability determine if we should accept a higher value over a lower
|
34
|
+
# value
|
35
|
+
def prefer?(cooled_metal)
|
36
|
+
return true if cooled_metal.energy < energy
|
37
|
+
|
38
|
+
energy_delta = energy - cooled_metal.energy
|
39
|
+
(Math::E**(energy_delta / cooled_metal.temperature)) > rand
|
40
|
+
end
|
41
|
+
|
42
|
+
def cool(new_temperature)
|
43
|
+
next_state = configuration.state_change.call(state)
|
44
|
+
Metal.new(next_state, new_temperature, configuration)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/annealing/simulator.rb
CHANGED
@@ -3,36 +3,50 @@
|
|
3
3
|
module Annealing
|
4
4
|
# It runs simulated annealing
|
5
5
|
class Simulator
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :configuration
|
7
7
|
|
8
|
-
def initialize(
|
9
|
-
@
|
10
|
-
@cooling_rate = cooling_rate || Annealing.configuration.cooling_rate
|
11
|
-
|
12
|
-
raise 'Invalid initial temperature' if @temperature.negative?
|
13
|
-
|
14
|
-
normalize_cooling_rate
|
8
|
+
def initialize(config_hash = {})
|
9
|
+
@configuration = Annealing.configuration.merge(config_hash)
|
15
10
|
end
|
16
11
|
|
17
|
-
def run(
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
def run(initial_state, config_hash = {})
|
13
|
+
with_runtime_config(config_hash) do |runtime_config|
|
14
|
+
initial_temperature = runtime_config.temperature
|
15
|
+
current = Metal.new(initial_state, initial_temperature, runtime_config)
|
16
|
+
steps = 0
|
17
|
+
until termination_condition_met?(current, runtime_config)
|
18
|
+
steps += 1
|
19
|
+
current = reduce_temperature(current, steps, runtime_config)
|
20
|
+
end
|
21
|
+
current
|
23
22
|
end
|
24
|
-
Annealing.logger.debug("Optimized: #{best}")
|
25
|
-
best
|
26
23
|
end
|
27
24
|
|
28
25
|
private
|
29
26
|
|
30
|
-
|
31
|
-
|
27
|
+
# Wrapper for public methods that may use a custom configuration
|
28
|
+
def with_runtime_config(config_hash)
|
29
|
+
runtime_config = if config_hash.is_a?(Hash) && config_hash.any?
|
30
|
+
configuration.merge(config_hash)
|
31
|
+
else
|
32
|
+
configuration
|
33
|
+
end
|
34
|
+
runtime_config.validate!
|
35
|
+
yield(runtime_config)
|
36
|
+
end
|
37
|
+
|
38
|
+
def reduce_temperature(metal, steps, config)
|
39
|
+
new_temperature = config.cool_down.call(metal.energy,
|
40
|
+
metal.temperature,
|
41
|
+
config.cooling_rate,
|
42
|
+
steps)
|
43
|
+
metal.cool!(new_temperature)
|
32
44
|
end
|
33
45
|
|
34
|
-
def
|
35
|
-
|
46
|
+
def termination_condition_met?(metal, config)
|
47
|
+
config.termination_condition.call(metal.state,
|
48
|
+
metal.energy,
|
49
|
+
metal.temperature)
|
36
50
|
end
|
37
51
|
end
|
38
52
|
end
|
data/lib/annealing/version.rb
CHANGED
data/lib/annealing.rb
CHANGED
@@ -1,16 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'logger'
|
4
|
-
require 'annealing/version'
|
5
|
-
require 'annealing/configuration'
|
6
|
-
require 'annealing/pool'
|
7
|
-
require 'annealing/simulator'
|
8
|
-
|
9
3
|
# Simulated Annealing algoritm
|
10
4
|
# https://en.wikipedia.org/wiki/Simulated_annealing
|
11
5
|
module Annealing
|
12
6
|
# Default error class
|
13
7
|
class Error < StandardError; end
|
8
|
+
|
14
9
|
class << self
|
15
10
|
attr_writer :configuration
|
16
11
|
end
|
@@ -21,16 +16,15 @@ module Annealing
|
|
21
16
|
|
22
17
|
def self.configure
|
23
18
|
yield(configuration)
|
19
|
+
configuration
|
24
20
|
end
|
25
21
|
|
26
|
-
def self.simulate(
|
27
|
-
|
28
|
-
|
29
|
-
simulator = Simulator.new
|
30
|
-
simulator.run(collection).collection
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.logger
|
34
|
-
configuration.logger
|
22
|
+
def self.simulate(initial_state, config_hash = {})
|
23
|
+
Simulator.new.run(initial_state, config_hash).state
|
35
24
|
end
|
36
25
|
end
|
26
|
+
|
27
|
+
require "annealing/configuration"
|
28
|
+
require "annealing/metal"
|
29
|
+
require "annealing/simulator"
|
30
|
+
require "annealing/version"
|
metadata
CHANGED
@@ -1,26 +1,49 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: annealing
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Luis Ezcurdia
|
7
|
+
- Luis Ezcurdia
|
8
|
+
- Chris Bloom
|
8
9
|
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
12
|
+
date: 2023-03-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ruby-prof
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.6'
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.6.1
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '1.6'
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.6.1
|
13
34
|
description: Simulated Annealing algoritm implementation.
|
14
35
|
email:
|
15
36
|
- ing.ezcurdia@gmail.com
|
37
|
+
- chrisbloom7@gmail.com
|
16
38
|
executables: []
|
17
39
|
extensions: []
|
18
40
|
extra_rdoc_files: []
|
19
41
|
files:
|
42
|
+
- ".github/workflows/ruby.yml"
|
20
43
|
- ".gitignore"
|
21
44
|
- ".rubocop.yml"
|
22
45
|
- ".rubocop_todo.yml"
|
23
|
-
-
|
46
|
+
- CHANGELOG.md
|
24
47
|
- CODE_OF_CONDUCT.md
|
25
48
|
- Gemfile
|
26
49
|
- Gemfile.lock
|
@@ -29,10 +52,13 @@ files:
|
|
29
52
|
- Rakefile
|
30
53
|
- annealing.gemspec
|
31
54
|
- bin/console
|
55
|
+
- bin/run
|
32
56
|
- bin/setup
|
33
57
|
- lib/annealing.rb
|
34
58
|
- lib/annealing/configuration.rb
|
35
|
-
- lib/annealing/
|
59
|
+
- lib/annealing/configuration/coolers.rb
|
60
|
+
- lib/annealing/configuration/terminators.rb
|
61
|
+
- lib/annealing/metal.rb
|
36
62
|
- lib/annealing/simulator.rb
|
37
63
|
- lib/annealing/version.rb
|
38
64
|
homepage: https://github.com/3zcurdia/annealing
|
@@ -41,6 +67,8 @@ licenses:
|
|
41
67
|
metadata:
|
42
68
|
homepage_uri: https://github.com/3zcurdia/annealing
|
43
69
|
source_code_uri: https://github.com/3zcurdia/annealing
|
70
|
+
changelog_uri: https://github.com/3zcurdia/annealing/blob/main/CHANGELOG.md
|
71
|
+
rubygems_mfa_required: 'true'
|
44
72
|
post_install_message:
|
45
73
|
rdoc_options: []
|
46
74
|
require_paths:
|
@@ -49,15 +77,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
77
|
requirements:
|
50
78
|
- - ">="
|
51
79
|
- !ruby/object:Gem::Version
|
52
|
-
version:
|
80
|
+
version: 3.0.0
|
53
81
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
82
|
requirements:
|
55
83
|
- - ">="
|
56
84
|
- !ruby/object:Gem::Version
|
57
85
|
version: '0'
|
58
86
|
requirements: []
|
59
|
-
rubygems_version: 3.
|
87
|
+
rubygems_version: 3.4.6
|
60
88
|
signing_key:
|
61
89
|
specification_version: 4
|
62
|
-
summary: Simulated Annealing
|
90
|
+
summary: Simulated Annealing
|
63
91
|
test_files: []
|
data/.travis.yml
DELETED
data/lib/annealing/pool.rb
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Annealing
|
4
|
-
# It manages the total energy of a given collection
|
5
|
-
class Pool
|
6
|
-
attr_reader :collection
|
7
|
-
|
8
|
-
def initialize(collection, energy_calculator = nil)
|
9
|
-
@collection = collection.dup
|
10
|
-
@energy_calculator = energy_calculator || Annealing.configuration.total_energy_calculator
|
11
|
-
end
|
12
|
-
|
13
|
-
def energy
|
14
|
-
@energy ||= energy_calculator.call(collection)
|
15
|
-
end
|
16
|
-
|
17
|
-
def better_than?(pool)
|
18
|
-
energy < pool.energy
|
19
|
-
end
|
20
|
-
|
21
|
-
def solution_at(temperature)
|
22
|
-
move = self.next
|
23
|
-
energy_delta = energy - move.energy
|
24
|
-
if energy_delta.positive? || (Math::E**(energy_delta / temperature)) > rand
|
25
|
-
move
|
26
|
-
else
|
27
|
-
self
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def to_s
|
32
|
-
format('%<energy>.4f:%<value>s', energy: energy, value: collection)
|
33
|
-
end
|
34
|
-
|
35
|
-
def next
|
36
|
-
Pool.new(swap_collection, energy_calculator)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
attr_reader :energy_calculator
|
42
|
-
|
43
|
-
def swap_collection
|
44
|
-
swapped = collection.dup
|
45
|
-
idx_a = rand(size)
|
46
|
-
idx_b = rand(size)
|
47
|
-
swapped[idx_b], swapped[idx_a] = swapped[idx_a], swapped[idx_b]
|
48
|
-
swapped
|
49
|
-
end
|
50
|
-
|
51
|
-
def size
|
52
|
-
collection.size
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|