annealing 0.2.0 → 0.3.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 +32 -10
- data/CHANGELOG.md +25 -0
- data/Gemfile +8 -3
- data/Gemfile.lock +46 -4
- data/LICENSE +1 -1
- data/README.md +197 -36
- data/Rakefile +17 -6
- data/annealing.gemspec +17 -14
- data/bin/console +3 -3
- data/bin/run +58 -0
- data/lib/annealing/configuration/configurator.rb +41 -0
- data/lib/annealing/configuration.rb +18 -7
- data/lib/annealing/metal.rb +63 -0
- data/lib/annealing/simulator.rb +51 -19
- data/lib/annealing/version.rb +1 -1
- data/lib/annealing.rb +9 -10
- metadata +36 -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: 8b20378dea6d3ebcee0d0adca58d721576605d17c34c91a1828f887733c5fc5e
|
4
|
+
data.tar.gz: 62935118c575c16e8e0efaaf58224845fc8e975f230ac58b81da41cf66a9c08c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4916ea9c5c854ce9891b9264d7dfe117f56da58fb906735f2afb015f2a0acf08757702991d988ed82096910225cca25794b78e135635d03b41dcf51496543a72
|
7
|
+
data.tar.gz: f3c74cb98d4b45512fd78bf6e18dfc751550ba33f6ccfa4ab80720d01dcdf4a1a0ec7215387033523792a503876e4abd6bc7b017330539a8367bed3da3a23741
|
@@ -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: ['2.6']
|
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: ['2.6', '2.7', '3.0', '3.1']
|
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: 2.6
|
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,42 @@
|
|
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-02-18 23:32:32 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.
|
11
|
-
|
9
|
+
# Offense count: 9
|
10
|
+
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes, Max.
|
11
|
+
Metrics/AbcSize:
|
12
|
+
Exclude:
|
13
|
+
- 'test/annealing/configuration/configurator_test.rb'
|
14
|
+
- 'test/annealing/metal_test.rb'
|
15
|
+
- 'test/annealing/simulator_test.rb'
|
16
|
+
- 'test/annealing_test.rb'
|
12
17
|
|
13
|
-
# Offense count:
|
14
|
-
# Configuration parameters: CountComments,
|
18
|
+
# Offense count: 3
|
19
|
+
# Configuration parameters: CountComments, Max, CountAsOne.
|
20
|
+
Metrics/ClassLength:
|
21
|
+
Exclude:
|
22
|
+
- 'test/annealing/configuration/configurator_test.rb'
|
23
|
+
- 'test/annealing/metal_test.rb'
|
24
|
+
- 'test/annealing/simulator_test.rb'
|
15
25
|
|
26
|
+
# Offense count: 13
|
27
|
+
# Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
|
28
|
+
Metrics/MethodLength:
|
29
|
+
Exclude:
|
30
|
+
- 'lib/annealing/configuration.rb'
|
31
|
+
- 'lib/annealing/simulator.rb'
|
32
|
+
- 'test/annealing/configuration/configurator_test.rb'
|
33
|
+
- 'test/annealing/metal_test.rb'
|
34
|
+
- 'test/annealing/simulator_test.rb'
|
35
|
+
- 'test/annealing_test.rb'
|
16
36
|
|
17
|
-
# Offense count:
|
18
|
-
|
37
|
+
# Offense count: 8
|
38
|
+
# Configuration parameters: Max.
|
39
|
+
Minitest/MultipleAssertions:
|
19
40
|
Exclude:
|
20
|
-
- 'test
|
41
|
+
- 'test/annealing/configuration/configurator_test.rb'
|
42
|
+
- 'test/annealing/configuration_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", ">= 13.0.6"
|
11
|
+
gem "rubocop", "~> 1.23"
|
12
|
+
gem "rubocop-minitest", "~> 0.17.0"
|
13
|
+
gem "rubocop-performance", "~> 1.12"
|
14
|
+
gem "rubocop-rake", "~> 0.6.0"
|
data/Gemfile.lock
CHANGED
@@ -1,21 +1,63 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
annealing (0.
|
4
|
+
annealing (0.2.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.5.11)
|
14
|
+
irb (1.4.1)
|
15
|
+
reline (>= 0.3.0)
|
16
|
+
minitest (5.14.4)
|
17
|
+
parallel (1.21.0)
|
18
|
+
parser (3.0.3.2)
|
19
|
+
ast (~> 2.4.1)
|
20
|
+
rainbow (3.0.0)
|
21
|
+
rake (13.0.6)
|
22
|
+
regexp_parser (2.2.0)
|
23
|
+
reline (0.3.1)
|
24
|
+
io-console (~> 0.5)
|
25
|
+
rexml (3.2.5)
|
26
|
+
rubocop (1.23.0)
|
27
|
+
parallel (~> 1.10)
|
28
|
+
parser (>= 3.0.0.0)
|
29
|
+
rainbow (>= 2.2.2, < 4.0)
|
30
|
+
regexp_parser (>= 1.8, < 3.0)
|
31
|
+
rexml
|
32
|
+
rubocop-ast (>= 1.12.0, < 2.0)
|
33
|
+
ruby-progressbar (~> 1.7)
|
34
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
35
|
+
rubocop-ast (1.14.0)
|
36
|
+
parser (>= 3.0.1.1)
|
37
|
+
rubocop-minitest (0.17.0)
|
38
|
+
rubocop (>= 0.90, < 2.0)
|
39
|
+
rubocop-performance (1.12.0)
|
40
|
+
rubocop (>= 1.7.0, < 2.0)
|
41
|
+
rubocop-ast (>= 0.4.0)
|
42
|
+
rubocop-rake (0.6.0)
|
43
|
+
rubocop (~> 1.0)
|
44
|
+
ruby-prof (1.4.3)
|
45
|
+
ruby-progressbar (1.11.0)
|
46
|
+
unicode-display_width (2.1.0)
|
11
47
|
|
12
48
|
PLATFORMS
|
13
49
|
ruby
|
14
50
|
|
15
51
|
DEPENDENCIES
|
16
52
|
annealing!
|
53
|
+
debug (>= 1.0.0)
|
17
54
|
minitest (~> 5.0)
|
18
|
-
rake (~>
|
55
|
+
rake (~> 13.0, >= 13.0.6)
|
56
|
+
rubocop (~> 1.23)
|
57
|
+
rubocop-minitest (~> 0.17.0)
|
58
|
+
rubocop-performance (~> 1.12)
|
59
|
+
rubocop-rake (~> 0.6.0)
|
60
|
+
ruby-prof (~> 1.4, >= 1.4.3)
|
19
61
|
|
20
62
|
BUNDLED WITH
|
21
63
|
2.1.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,219 @@ 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
|
-
|
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.
|
90
|
+
|
91
|
+
```ruby
|
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)]
|
97
|
+
```
|
98
|
+
|
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 specify a custom `cool_down` function. The function can be any object that responds to `#call` and accepts three 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.
|
64
106
|
|
65
107
|
```ruby
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
solution.collection
|
71
|
-
[(20,40), (40,120), (100,120), (60,200), (180,200)]
|
108
|
+
Annealing.configuration.cool_down = lambda do |_energy, temperature, cooling_rate, steps|
|
109
|
+
# Reduce temperature exponentially
|
110
|
+
temperature - (cooling_rate * (steps**2))
|
111
|
+
end
|
72
112
|
```
|
73
113
|
|
74
|
-
|
114
|
+
### `cooling_rate` and `temperature`
|
115
|
+
|
116
|
+
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`.
|
75
117
|
|
76
118
|
```ruby
|
77
|
-
Annealing.configure do |
|
78
|
-
|
79
|
-
|
119
|
+
Annealing.configure do |config|
|
120
|
+
config.cooling_rate = 0.001
|
121
|
+
config.temperature = 25_000
|
80
122
|
end
|
81
123
|
```
|
82
124
|
|
83
|
-
|
125
|
+
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.
|
84
126
|
|
85
|
-
|
127
|
+
### `energy_calculator`
|
128
|
+
|
129
|
+
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.
|
86
130
|
|
87
131
|
```ruby
|
88
|
-
|
89
|
-
|
90
|
-
|
132
|
+
# A custom calculator class that takes into account hypothetical external factors
|
133
|
+
class PotentialSalesCalculator
|
134
|
+
def initialize(initial_time_of_day)
|
135
|
+
@initial_time_of_day = initial_time_of_day
|
136
|
+
end
|
137
|
+
|
138
|
+
def energy(locations)
|
139
|
+
arrival_time = @initial_time_of_day
|
140
|
+
first_location_sales = potential_sales(locations.first, arrival_time)
|
141
|
+
locations.each_cons(2).sum do |location1, location2|
|
142
|
+
arrival_time += travel_time(location1, location2, arrival_time)
|
143
|
+
distance = location1.distance(location2)
|
144
|
+
potential_sales(locations.first, arrival_time) / distance
|
145
|
+
end + first_location_sales
|
146
|
+
end
|
147
|
+
|
148
|
+
def potential_sales(location, time_of_day)
|
149
|
+
habits = CustomerHabits.new(location)
|
150
|
+
customers = habits.whos_home_at(time_of_day)
|
151
|
+
SalesTrends.estimate(customers)
|
152
|
+
end
|
153
|
+
|
154
|
+
def travel_time(location1, location2, time_of_day)
|
155
|
+
traffic = TrafficPredictor.new(location1, location2)
|
156
|
+
traffic.travel_time(time_of_day)
|
91
157
|
end
|
92
158
|
end
|
159
|
+
|
160
|
+
calculator = PotentialSalesCalculator.new(8)
|
161
|
+
Annealing.configuration.energy_calculator = calculator.method(:energy)
|
93
162
|
```
|
94
163
|
|
95
|
-
|
164
|
+
### `logger`
|
165
|
+
|
166
|
+
The default logger is a standard Ruby Logger that writes to stdout. You can specify a different `logger` if you wish.
|
96
167
|
|
97
168
|
```ruby
|
98
|
-
|
99
|
-
|
169
|
+
logger = Logger.new('logfile.log')
|
170
|
+
logger.level = Logger::DEBUG
|
171
|
+
Annealing.configuration.logger = logger
|
172
|
+
```
|
173
|
+
|
174
|
+
### `state_change`
|
175
|
+
|
176
|
+
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.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class MyClass
|
180
|
+
def state_change(state)
|
181
|
+
size = state.size
|
182
|
+
swapped = state.dup
|
183
|
+
idx_a = rand(size)
|
184
|
+
idx_b = rand(size)
|
185
|
+
swapped[idx_b], swapped[idx_a] = swapped[idx_a], swapped[idx_b]
|
186
|
+
swapped
|
187
|
+
end
|
100
188
|
end
|
101
|
-
|
102
|
-
|
189
|
+
|
190
|
+
instance = MyClass.new
|
191
|
+
Annealing.configuration.state_change = instance.method(:state_change)
|
192
|
+
```
|
193
|
+
|
194
|
+
### `termination_condition`
|
195
|
+
|
196
|
+
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. You can define a custom `termination_condition` function, which 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.
|
197
|
+
|
198
|
+
```ruby
|
199
|
+
Annealing.configuration.termination_condition = lambda do |_state, energy, temperature|
|
200
|
+
# Stop early if we encounter any 0-energy state
|
201
|
+
energy == 0 || temperature <= 0.0
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
## Configuration precedence
|
206
|
+
|
207
|
+
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.
|
208
|
+
|
209
|
+
### Global configuration
|
210
|
+
|
211
|
+
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:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
# Set globally using block style
|
215
|
+
Annealing.configure do |config|
|
216
|
+
config.energy_calculator = energy_calculator
|
217
|
+
end
|
218
|
+
|
219
|
+
# Or set individually
|
220
|
+
Annealing.configuration.state_change = state_change
|
221
|
+
|
222
|
+
# Now we don't need to specify them just in time
|
223
|
+
solution = Annealing.simulate(locations)
|
224
|
+
```
|
225
|
+
|
226
|
+
### Instance configuration
|
227
|
+
|
228
|
+
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.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
Annealing.configure do |config|
|
232
|
+
config.energy_calculator = energy_calculator
|
233
|
+
config.state_change = state_change
|
234
|
+
config.temperature = 10_000
|
235
|
+
end
|
236
|
+
|
237
|
+
simulation = Annealing::Simulator.new(temperature: 1_000)
|
238
|
+
simulation.run(locations) # Will use an initial temperature value of 1000
|
239
|
+
simulation.run(locations.shuffle) # So will this
|
240
|
+
```
|
241
|
+
|
242
|
+
### Just-in-time configuration
|
243
|
+
|
244
|
+
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.
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
Annealing.configure do |config|
|
248
|
+
config.cooling_rate = 0.001
|
249
|
+
config.energy_calculator = energy_calculator
|
250
|
+
config.state_change = state_change
|
251
|
+
config.temperature = 10_000
|
252
|
+
end
|
253
|
+
|
254
|
+
# Will use an initial temperature of 20,000 and a cooling rate of 0.001
|
255
|
+
solution = Annealing.simulate(locations, temperature: 20_000)
|
256
|
+
|
257
|
+
# Set an instance cooling rate of 0.002
|
258
|
+
simulation = Annealing::Simulator.new(cooling_rate: 0.002)
|
259
|
+
|
260
|
+
# Will use an initial temperature of 20,000 and a cooling rate of 0.002
|
261
|
+
simulation.run(locations, temperature: 20_000)
|
262
|
+
|
263
|
+
# Will use an initial temperature of 10,000 and a cooling rate of 0.003
|
264
|
+
simulation.run(locations.shuffle, cooling_rate: 0.003)
|
103
265
|
```
|
104
266
|
|
105
267
|
## Development
|
106
268
|
|
107
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
|
269
|
+
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
270
|
|
109
271
|
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
272
|
|
111
273
|
## Contributing
|
112
274
|
|
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
|
-
|
275
|
+
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
276
|
|
116
277
|
## Code of Conduct
|
117
278
|
|
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(">= 2.6.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.4", ">= 1.4.3"
|
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,58 @@
|
|
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
|
+
Annealing.configuration.logger.level = Logger::DEBUG
|
43
|
+
simulator = Annealing::Simulator.new(temperature: 10_000, cooling_rate: 0.01)
|
44
|
+
solution = simulator.run(locations,
|
45
|
+
energy_calculator: energy_calculator,
|
46
|
+
state_change: state_change)
|
47
|
+
|
48
|
+
puts "\nInitial itinerary:"
|
49
|
+
locations.each_cons(2).each_with_index do |(location1, location2), index|
|
50
|
+
puts "Stop ##{index + 1}: #{location1.name} -> #{location2.name} (#{location1.distance(location2)})"
|
51
|
+
end
|
52
|
+
puts "-------\nEnergy: #{energy_calculator.call(locations)}"
|
53
|
+
|
54
|
+
puts "\nAnnealed itinerary:"
|
55
|
+
solution.state.each_cons(2).each_with_index do |(location1, location2), index|
|
56
|
+
puts "Stop ##{index + 1}: #{location1.name} -> #{location2.name} (#{location1.distance(location2)})"
|
57
|
+
end
|
58
|
+
puts "-------\nEnergy: #{solution.energy}"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Annealing
|
4
|
+
class Configuration
|
5
|
+
# Configuration mixin
|
6
|
+
module Configurator
|
7
|
+
def self.included(base)
|
8
|
+
base.send :include, InstanceMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
# Mixin methods
|
12
|
+
module InstanceMethods
|
13
|
+
def init_configuration(config_hash = {})
|
14
|
+
@instance_configuration = config_hash
|
15
|
+
@temporary_configuration = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_configuration_overrides(local_config_hash = {})
|
19
|
+
@temporary_configuration = local_config_hash
|
20
|
+
yield
|
21
|
+
ensure
|
22
|
+
@temporary_configuration = {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def configuration_overrides
|
26
|
+
instance_configuration.merge(temporary_configuration)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_accessor :instance_configuration, :temporary_configuration
|
32
|
+
|
33
|
+
def current_config_for(config)
|
34
|
+
temporary_configuration[config] ||
|
35
|
+
instance_configuration[config] ||
|
36
|
+
Annealing.configuration.public_send(config)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -3,17 +3,28 @@
|
|
3
3
|
module Annealing
|
4
4
|
# It enables the gem configuration
|
5
5
|
class Configuration
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :cool_down,
|
7
|
+
:cooling_rate,
|
8
|
+
:energy_calculator,
|
9
|
+
:logger,
|
10
|
+
:state_change,
|
11
|
+
:temperature,
|
12
|
+
:termination_condition
|
7
13
|
|
8
14
|
def initialize
|
9
|
-
@
|
15
|
+
@cool_down = lambda do |_energy, temperature, cooling_rate, _steps|
|
16
|
+
# Reduce the temperature linearly by default
|
17
|
+
temperature - cooling_rate
|
18
|
+
end
|
10
19
|
@cooling_rate = 0.0003
|
11
|
-
@
|
12
|
-
|
13
|
-
|
14
|
-
|
20
|
+
@energy_calculator = nil
|
21
|
+
@logger = Logger.new($stdout, level: Logger::INFO)
|
22
|
+
@state_change = nil
|
23
|
+
@temperature = 10_000.0
|
24
|
+
@termination_condition = lambda do |_state, _energy, temperature|
|
25
|
+
# Terminate the simulation as soon as the temperature reaches 0
|
26
|
+
temperature <= 0.0
|
15
27
|
end
|
16
|
-
@logger = Logger.new(STDOUT)
|
17
28
|
end
|
18
29
|
end
|
19
30
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Annealing
|
4
|
+
# It manages the total energy of a given collection
|
5
|
+
class Metal
|
6
|
+
include Configuration::Configurator
|
7
|
+
attr_reader :state, :temperature
|
8
|
+
|
9
|
+
def initialize(current_state, current_temperature, **config)
|
10
|
+
init_configuration(config)
|
11
|
+
@state = current_state
|
12
|
+
@temperature = current_temperature
|
13
|
+
|
14
|
+
raise(ArgumentError, "Missing energy calculator function") unless energy_calculator.respond_to?(:call)
|
15
|
+
|
16
|
+
raise(ArgumentError, "Missing state change function") unless state_change.respond_to?(:call)
|
17
|
+
end
|
18
|
+
|
19
|
+
def energy
|
20
|
+
@energy ||= energy_calculator.call(state)
|
21
|
+
end
|
22
|
+
|
23
|
+
# This method is not idempotent!
|
24
|
+
# It relies on random probability to select the next state
|
25
|
+
def cool!(new_temperature)
|
26
|
+
cooled_metal = cool(new_temperature)
|
27
|
+
if better_than?(cooled_metal)
|
28
|
+
cooled_metal
|
29
|
+
else
|
30
|
+
@temperature = new_temperature
|
31
|
+
self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
format("%<temperature>.4f:%<energy>.4f:%<value>s",
|
37
|
+
temperature: temperature,
|
38
|
+
energy: energy,
|
39
|
+
value: state)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def energy_calculator
|
45
|
+
current_config_for(:energy_calculator)
|
46
|
+
end
|
47
|
+
|
48
|
+
def state_change
|
49
|
+
current_config_for(:state_change)
|
50
|
+
end
|
51
|
+
|
52
|
+
def better_than?(cooled_metal)
|
53
|
+
energy_delta = energy - cooled_metal.energy
|
54
|
+
energy_delta.positive? ||
|
55
|
+
(Math::E**(energy_delta / cooled_metal.temperature)) > rand
|
56
|
+
end
|
57
|
+
|
58
|
+
def cool(new_temperature)
|
59
|
+
next_state = state_change.call(state)
|
60
|
+
Metal.new(next_state, new_temperature, **configuration_overrides)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/annealing/simulator.rb
CHANGED
@@ -3,36 +3,68 @@
|
|
3
3
|
module Annealing
|
4
4
|
# It runs simulated annealing
|
5
5
|
class Simulator
|
6
|
-
|
6
|
+
include Configuration::Configurator
|
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)
|
9
|
+
init_configuration(config)
|
15
10
|
end
|
16
11
|
|
17
|
-
def run(
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
12
|
+
def run(initial_state, config_hash = {})
|
13
|
+
with_configuration_overrides(config_hash) do
|
14
|
+
validate_configuration!
|
15
|
+
current = Metal.new(initial_state, temperature,
|
16
|
+
**configuration_overrides)
|
17
|
+
Annealing.logger.debug("Original: #{current}")
|
18
|
+
steps = 0
|
19
|
+
until termination_condition_met?(termination_condition, current)
|
20
|
+
steps += 1
|
21
|
+
current = reduce_temperature(cool_down, current, steps)
|
22
|
+
end
|
23
|
+
Annealing.logger.debug("Optimized: #{current}")
|
24
|
+
current
|
23
25
|
end
|
24
|
-
Annealing.logger.debug("Optimized: #{best}")
|
25
|
-
best
|
26
26
|
end
|
27
27
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def cool_down
|
31
|
-
(
|
31
|
+
current_config_for(:cool_down)
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
35
|
-
|
34
|
+
def cooling_rate
|
35
|
+
current_config_for(:cooling_rate).to_f
|
36
|
+
end
|
37
|
+
|
38
|
+
def logger
|
39
|
+
current_config_for(:logger)
|
40
|
+
end
|
41
|
+
|
42
|
+
def temperature
|
43
|
+
current_config_for(:temperature).to_f
|
44
|
+
end
|
45
|
+
|
46
|
+
def termination_condition
|
47
|
+
current_config_for(:termination_condition)
|
48
|
+
end
|
49
|
+
|
50
|
+
def reduce_temperature(cool_down, metal, steps)
|
51
|
+
new_temperature = cool_down.call(metal.energy, metal.temperature,
|
52
|
+
cooling_rate, steps)
|
53
|
+
metal.cool!(new_temperature)
|
54
|
+
end
|
55
|
+
|
56
|
+
def termination_condition_met?(termination_condition, metal)
|
57
|
+
termination_condition.call(metal.state, metal.energy, metal.temperature)
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_configuration!
|
61
|
+
raise(ArgumentError, "Invalid initial temperature") if temperature.negative?
|
62
|
+
|
63
|
+
raise(ArgumentError, "Invalid initial cooling rate") if cooling_rate.negative?
|
64
|
+
|
65
|
+
raise(ArgumentError, "Missing cool down function") unless cool_down.respond_to?(:call)
|
66
|
+
|
67
|
+
raise(ArgumentError, "Missing termination condition function") unless termination_condition.respond_to?(:call)
|
36
68
|
end
|
37
69
|
end
|
38
70
|
end
|
data/lib/annealing/version.rb
CHANGED
data/lib/annealing.rb
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
3
|
+
require "logger"
|
4
|
+
require "annealing/version"
|
5
|
+
require "annealing/configuration"
|
6
|
+
require "annealing/configuration/configurator"
|
7
|
+
require "annealing/metal"
|
8
|
+
require "annealing/simulator"
|
8
9
|
|
9
10
|
# Simulated Annealing algoritm
|
10
11
|
# https://en.wikipedia.org/wiki/Simulated_annealing
|
11
12
|
module Annealing
|
12
13
|
# Default error class
|
13
14
|
class Error < StandardError; end
|
15
|
+
|
14
16
|
class << self
|
15
17
|
attr_writer :configuration
|
16
18
|
end
|
@@ -23,11 +25,8 @@ module Annealing
|
|
23
25
|
yield(configuration)
|
24
26
|
end
|
25
27
|
|
26
|
-
def self.simulate(
|
27
|
-
|
28
|
-
|
29
|
-
simulator = Simulator.new
|
30
|
-
simulator.run(collection).collection
|
28
|
+
def self.simulate(initial_state, config_hash = {})
|
29
|
+
Simulator.new.run(initial_state, config_hash).state
|
31
30
|
end
|
32
31
|
|
33
32
|
def self.logger
|
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.3.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: 2022-02-22 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.4'
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.4.3
|
24
|
+
type: :development
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '1.4'
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.4.3
|
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,12 @@ 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/configurator.rb
|
60
|
+
- lib/annealing/metal.rb
|
36
61
|
- lib/annealing/simulator.rb
|
37
62
|
- lib/annealing/version.rb
|
38
63
|
homepage: https://github.com/3zcurdia/annealing
|
@@ -41,6 +66,8 @@ licenses:
|
|
41
66
|
metadata:
|
42
67
|
homepage_uri: https://github.com/3zcurdia/annealing
|
43
68
|
source_code_uri: https://github.com/3zcurdia/annealing
|
69
|
+
changelog_uri: https://github.com/3zcurdia/annealing/blob/main/CHANGELOG.md
|
70
|
+
rubygems_mfa_required: 'true'
|
44
71
|
post_install_message:
|
45
72
|
rdoc_options: []
|
46
73
|
require_paths:
|
@@ -49,15 +76,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
76
|
requirements:
|
50
77
|
- - ">="
|
51
78
|
- !ruby/object:Gem::Version
|
52
|
-
version: 2.
|
79
|
+
version: 2.6.0
|
53
80
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
81
|
requirements:
|
55
82
|
- - ">="
|
56
83
|
- !ruby/object:Gem::Version
|
57
84
|
version: '0'
|
58
85
|
requirements: []
|
59
|
-
rubygems_version: 3.
|
86
|
+
rubygems_version: 3.3.7
|
60
87
|
signing_key:
|
61
88
|
specification_version: 4
|
62
|
-
summary: Simulated Annealing
|
89
|
+
summary: Simulated Annealing
|
63
90
|
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
|