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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 53e5900add60de36b8e3bb441be9bce450f6dcb3cbd67552913684745e90a0c1
4
- data.tar.gz: 00347f761988a73951ed950317c1fc6ebf6d1add2d2ab1889bf8cbe6eebabe36
3
+ metadata.gz: c5435b7e34667fdfd63782d895080435f7f2dffb685b2a918bba83bf6781f432
4
+ data.tar.gz: 82bfb71e31afed6d53e0649b35c373fc5ea27bcc19e6b72ae8522983df1d4734
5
5
  SHA512:
6
- metadata.gz: ef3653ce209af0a76189157cdecbbac58613aa86845b0e8476a22407fc17b9edb75e879f1676497eb7f1d8b4f86549719f67825f7c48db134603ed784d2d5ef1
7
- data.tar.gz: 022accd062db4cc155e50606d22ea7e51db7603a61ef9b40512625110d528975de275b1abbb5ea6604fa98162122ef8b5e143f474593536af4291089e4d3f229
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
- Metrics/AbcSize:
4
- Max: 25
5
- Metrics/MethodLength:
6
- Max: 15
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
- Lint/RaiseException:
10
- Enabled: true
11
- Lint/StructNewOverride:
12
- Enabled: false
13
- Style/HashEachMethods:
14
- Enabled: true
15
- Style/HashTransformKeys:
18
+
19
+ Style/StringLiterals:
16
20
  Enabled: true
17
- Style/HashTransformValues:
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 2020-06-15 23:46:36 -0500 using RuboCop version 0.81.0.
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: 1
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: CountComments, ExcludedMethods.
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: 2
18
- Style/Documentation:
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 'https://rubygems.org'
3
+ source "https://rubygems.org"
4
4
 
5
5
  # Specify your gem's dependencies in annealing.gemspec
6
6
  gemspec
7
7
 
8
- gem 'minitest', '~> 5.0'
9
- gem 'rake', '~> 12.0'
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.1.0)
4
+ annealing (0.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
- minitest (5.14.1)
10
- rake (12.3.3)
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 (~> 12.0)
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.1.4
65
+ 2.4.4
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Luis Ezcurdia
3
+ Copyright (c) 2022 Luis Ezcurdia
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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 enumerable objects. Where the energy of each object can be measured by a distance method between two elements or a lambda function where the total energy is calculated in the objects.
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
- $ bundle install
18
+ ```shell
19
+ bundle install
20
+ ```
17
21
 
18
22
  Or install it yourself as:
19
23
 
20
- $ gem install annealing
24
+ ```shell
25
+ gem install annealing
26
+ ```
21
27
 
22
28
  ## Usage
23
29
 
24
- To use this gem, you must have an object that implements a `distance` method or a lambda function where you calculate the total energy of an enumerable object.
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 solve the traveling salesman, for that we will need an object with a distance method
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 locations
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
- Now we can just pass the locations argumen.
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
- puts Annealing.simulate(locations)
59
- [(20,40), (40,120), (100,120), (60,200), (180,200)]
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
- This will run the simulation with the default parameters and it could take a while to finish.
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
- simulator = Annealing::Simulator.new(temperature: 10_000, cooling_rate: 0.5)
67
- solution = simulator.run(locations)
68
- solution.energy
69
- 351.901234
70
- solution.collection
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
- You can also configure the default parameters
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
- Annealing.configure do |c|
78
- c.temperature = 10_000.0
79
- c.cooling_rate = 0.003
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
- ### Custom total energy calculator
128
+ ### `cooling_rate` and `temperature`
84
129
 
85
- You can set a lambda function to customize the total energy in a collection by setting in the default parameters
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 |c|
89
- c.total_energy_calculator = lambda do |enumerable|
90
- enumerable.each_cons(2).sum { |a, b| a.distance(b) }
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
- or in the simulator as a parameter
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
- calc = lambda do |enumerable|
99
- enumerable.each_cons(2).sum { |a, b| a.distance(b) }
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
- solution = simulator.run(locations, calc).collection
102
- [(20,40), (40,120), (100,120), (60,200), (180,200)]
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 test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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 'bundler/gem_tasks'
4
- require 'rake/testtask'
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 << 'test'
8
- t.libs << 'lib'
9
- t.test_files = FileList['test/**/*_test.rb']
8
+ t.libs << "test"
9
+ t.libs << "lib"
10
+ t.test_files = FileList["test/**/*_test.rb"]
10
11
  end
11
12
 
12
- task default: :test
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 'lib/annealing/version'
3
+ require_relative "lib/annealing/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = 'annealing'
6
+ spec.name = "annealing"
7
7
  spec.version = Annealing::VERSION
8
- spec.authors = ['Luis Ezcurdia Razo']
9
- spec.email = ['ing.ezcurdia@gmail.com']
10
- spec.license = 'MIT'
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 = 'Simulated Annealing algoritm'
13
- spec.description = 'Simulated Annealing algoritm implementation.'
14
- spec.homepage = 'https://github.com/3zcurdia/annealing'
15
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
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['homepage_uri'] = spec.homepage
20
- spec.metadata['source_code_uri'] = 'https://github.com/3zcurdia/annealing'
21
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
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 = 'exe'
28
+ spec.bindir = "exe"
29
29
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
- spec.require_paths = ['lib']
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 'bundler/setup'
5
- require 'annealing'
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 'irb'
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
- attr_accessor :temperature, :cooling_rate, :total_energy_calculator, :logger
7
-
8
- def initialize
9
- @temperature = 10_000.0
10
- @cooling_rate = 0.0003
11
- @total_energy_calculator = lambda do |enumerable|
12
- enumerable.each_cons(2).sum do |value_a, value_b|
13
- value_a.distance(value_b)
14
- end
15
- end
16
- @logger = Logger.new(STDOUT)
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
@@ -3,36 +3,50 @@
3
3
  module Annealing
4
4
  # It runs simulated annealing
5
5
  class Simulator
6
- attr_reader :temperature, :cooling_rate
6
+ attr_reader :configuration
7
7
 
8
- def initialize(temperature: nil, cooling_rate: nil)
9
- @temperature = temperature || Annealing.configuration.temperature
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(collection, calculator = nil)
18
- best = current = Pool.new(collection.shuffle, calculator)
19
- Annealing.logger.debug(" Original: #{current}")
20
- cool_down do |temp|
21
- current = current.solution_at(temp)
22
- best = current if current.better_than?(best)
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
- def cool_down
31
- (temperature..0).step(cooling_rate).each { |temp| yield temp }
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 normalize_cooling_rate
35
- @cooling_rate = -1.0 * cooling_rate if cooling_rate.positive?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Annealing
4
- VERSION = '0.2.0'
4
+ VERSION = "0.4.0"
5
5
  end
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(collection)
27
- return [] if collection.empty?
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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
- - Luis Ezcurdia Razo
7
+ - Luis Ezcurdia
8
+ - Chris Bloom
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2020-06-23 00:00:00.000000000 Z
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
- - ".travis.yml"
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/pool.rb
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: 2.3.0
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.1.3
87
+ rubygems_version: 3.4.6
60
88
  signing_key:
61
89
  specification_version: 4
62
- summary: Simulated Annealing algoritm
90
+ summary: Simulated Annealing
63
91
  test_files: []
data/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- ---
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.7.1
6
- before_install: gem install bundler -v 2.1.4
@@ -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