{"id":21689647,"url":"https://github.com/3zcurdia/annealing","last_synced_at":"2026-03-07T15:32:16.787Z","repository":{"id":43717732,"uuid":"272615265","full_name":"3zcurdia/annealing","owner":"3zcurdia","description":"Simulated annealing ruby implementation","archived":false,"fork":false,"pushed_at":"2025-09-19T20:02:12.000Z","size":96,"stargazers_count":12,"open_issues_count":2,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-09T04:29:10.311Z","etag":null,"topics":["algorithm","simulated-annealing"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/3zcurdia.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-16T05:12:10.000Z","updated_at":"2025-09-19T20:02:14.000Z","dependencies_parsed_at":"2024-05-17T16:33:09.407Z","dependency_job_id":"634b792c-4085-4b60-9e16-3e812f947e6c","html_url":"https://github.com/3zcurdia/annealing","commit_stats":{"total_commits":53,"total_committers":3,"mean_commits":"17.666666666666668","dds":"0.28301886792452835","last_synced_commit":"576bcb69d45c4481311f4e51330fdb06e723c825"},"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/3zcurdia/annealing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/3zcurdia%2Fannealing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/3zcurdia%2Fannealing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/3zcurdia%2Fannealing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/3zcurdia%2Fannealing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/3zcurdia","download_url":"https://codeload.github.com/3zcurdia/annealing/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/3zcurdia%2Fannealing/sbom","scorecard":{"id":3830,"data":{"date":"2025-08-11","repo":{"name":"github.com/3zcurdia/annealing","commit":"a02c723d89ca8bf23a8aad1590cea1b4e945a8cc"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.9,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":5,"reason":"Found 11/22 approved changesets -- score normalized to 5","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/ruby.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ruby.yml:23: update your workflow using https://app.stepsecurity.io/secureworkflow/3zcurdia/annealing/ruby.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ruby.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/3zcurdia/annealing/ruby.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/ruby.yml:38: update your workflow using https://app.stepsecurity.io/secureworkflow/3zcurdia/annealing/ruby.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/ruby.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/3zcurdia/annealing/ruby.yml/main?enable=pin","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   2 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 24 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-14T13:07:47.461Z","repository_id":43717732,"created_at":"2025-08-14T13:07:47.462Z","updated_at":"2025-08-14T13:07:47.462Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30212021,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-07T09:02:10.694Z","status":"ssl_error","status_checked_at":"2026-03-07T09:02:08.429Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["algorithm","simulated-annealing"],"created_at":"2024-11-25T17:27:05.983Z","updated_at":"2026-03-07T15:32:16.750Z","avatar_url":"https://github.com/3zcurdia.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Annealing\n\n[![Gem Version](https://badge.fury.io/rb/annealing.svg)](https://badge.fury.io/rb/annealing)\n[![Ruby](https://github.com/3zcurdia/annealing/actions/workflows/ruby.yml/badge.svg)](https://github.com/3zcurdia/annealing/actions/workflows/ruby.yml)\n\nFind the optimal solution in a complex problem through a simulated annealing implementation for Ruby objects.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'annealing'\n```\n\nAnd then execute:\n\n```shell\nbundle install\n```\n\nOr install it yourself as:\n\n```shell\ngem install annealing\n```\n\n## Usage\n\nSimulated 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.\n\nIn order to use this algorithm we must first define 3 things:\n\n1. an initial object state to evaluate\n2. a way to measure the energy of that state\n3. and a way to change the state of the object over time\n\nLets 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.\n\n```ruby\nLocation = Struct.new(:x, :y) do\n  def inspect\n    \"(#{x},#{y})\"\n  end\n\n  def distance(location)\n    dx = (x - location.x).abs\n    dy = (y - location.y).abs\n    Math.sqrt(dx**2 + dy**2)\n  end\nend\n```\n\nNow 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.\n\n```ruby\nlocations = [\n  Location.new(60, 200),\n  Location.new(180, 200),\n  Location.new(40, 120),\n  Location.new(100, 120),\n  Location.new(20, 40)\n].shuffle\n```\n\nNext 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.\n\n```ruby\nenergy_calculator = lambda do |locations|\n  locations.each_cons(2).sum do |location1, location2|\n    location1.distance(location2)\n  end\nend\n```\n\nFinally, 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.\n\n```ruby\nstate_change = lambda do |locations|\n  size = locations.size\n  swapped = locations.dup\n  idx_a = rand(size)\n  idx_b = rand(size)\n  swapped[idx_b], swapped[idx_a] = swapped[idx_a], swapped[idx_b]\n  swapped\nend\n```\n\nNow 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.\n\n```ruby\noptimal_route = Annealing.simulate(locations,\n                                   energy_calculator: energy_calculator,\n                                   state_change: state_change)\noptimal_route.state\n# =\u003e [(20,40), (40,120), (100,120), (60,200), (180,200)]\n```\n\n## Configuration options\n\nThe 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.\n\n### `cool_down`\n\nBy 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.\n\n```ruby\n# Use the built-in linear cool-down function (the default)\nAnnealing.configuration.cool_down = Annealing::Configuration::Coolers.linear\n\n# Use the built-in exponential cool-down function\nAnnealing.configuration.cool_down = Annealing::Configuration::Coolers.exponential\n\n# Use the built-in geometric cool-down function with a custom ratio (default ratio is 2)\nAnnealing.configuration.cool_down = Annealing::Configuration::Coolers.exponential(1.5)\n\n# Use a custom cool down function\nAnnealing.configuration.cool_down = lambda do |energy, temperature, cooling_rate, steps|\n  # Reduce temperature exponentially when the temperature is above 500, then linearly\n  if temperature \u003e 500\n    Annealing::Configuration::Coolers.exponential.call(energy, temperature, cooling_rate, steps)\n  else\n    Annealing::Configuration::Coolers.linear.call(energy, temperature, cooling_rate, steps)\n  end\nend\n```\n\n### `cooling_rate` and `temperature`\n\nIn 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`.\n\n```ruby\nAnnealing.configure do |config|\n  config.cooling_rate = 0.001\n  config.temperature = 25_000\nend\n```\n\nGenerally 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.\n\n### `energy_calculator`\n\nYou 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.\n\n```ruby\n# A custom calculator class that takes into account hypothetical external factors\nclass PotentialSalesCalculator\n  def initialize(initial_time_of_day)\n    @initial_time_of_day = initial_time_of_day\n  end\n\n  def energy(locations)\n    arrival_time = @initial_time_of_day\n    first_location_sales = potential_sales(locations.first, arrival_time)\n    locations.each_cons(2).sum do |location1, location2|\n      arrival_time += travel_time(location1, location2, arrival_time)\n      distance = location1.distance(location2)\n      potential_sales(locations.first, arrival_time) / distance\n    end + first_location_sales\n  end\n\n  def potential_sales(location, time_of_day)\n    habits = CustomerHabits.new(location)\n    customers = habits.whos_home_at(time_of_day)\n    SalesTrends.estimate(customers)\n  end\n\n  def travel_time(location1, location2, time_of_day)\n    traffic = TrafficPredictor.new(location1, location2)\n    traffic.travel_time(time_of_day)\n  end\nend\n\ncalculator = PotentialSalesCalculator.new(8)\nAnnealing.configuration.energy_calculator = calculator.method(:energy)\n```\n\n### `state_change`\n\nAs 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.\n\n```ruby\nclass MyClass\n  def state_change(state)\n    size = state.size\n    swapped = state.dup\n    idx_a = rand(size)\n    idx_b = rand(size)\n    swapped[idx_b], swapped[idx_a] = swapped[idx_a], swapped[idx_b]\n    swapped\n  end\nend\n\ninstance = MyClass.new\nAnnealing.configuration.state_change = instance.method(:state_change)\n```\n\n### `termination_condition`\n\nBy 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.\n\n```ruby\n# Use the built-in zero-temperature termination condition function\nAnnealing.configuration.termination_condition = Annealing::Configuration::Terminators.temp_is_zero?\n\n# Use the built-in zero-energy termination condition function\nAnnealing.configuration.termination_condition = Annealing::Configuration::Terminators.energy_or_temp_is_zero?\n\n# Use a custom termination condition function\nAnnealing.configuration.termination_condition = lambda do |_state, energy, temperature|\n  # Stop if the energy is below 500 and the temperature is below 100, or the temperature is already 0\n  temperature \u003c= 0 || (energy \u003c= 500 \u0026\u0026 temperature \u003c= 500)\nend\n```\n\n## Configuration precedence\n\nConfiguration 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.\n\n### Global configuration\n\nGlobal 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:\n\n```ruby\n# Set globally using block style\nAnnealing.configure do |config|\n  config.energy_calculator = energy_calculator\nend\n\n# Or set individually\nAnnealing.configuration.state_change = state_change\n\n# Now we don't need to specify them just in time\nsolution = Annealing.simulate(locations)\n```\n\n### Instance configuration\n\nInstance 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.\n\n```ruby\nAnnealing.configure do |config|\n  config.energy_calculator = energy_calculator\n  config.state_change = state_change\n  config.temperature = 10_000\nend\n\nsimulation = Annealing::Simulator.new(temperature: 1_000)\nsimulation.run(locations) # Will use an initial temperature value of 1000\nsimulation.run(locations.shuffle) # So will this\n```\n\n### Just-in-time configuration\n\nJust-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.\n\n```ruby\nAnnealing.configure do |config|\n  config.cooling_rate = 0.001\n  config.energy_calculator = energy_calculator\n  config.state_change = state_change\n  config.temperature = 10_000\nend\n\n# Will use an initial temperature of 20,000 and a cooling rate of 0.001\nsolution = Annealing.simulate(locations, temperature: 20_000)\n\n# Set an instance cooling rate of 0.002\nsimulation = Annealing::Simulator.new(cooling_rate: 0.002)\n\n# Will use an initial temperature of 20,000 and a cooling rate of 0.002\nsimulation.run(locations, temperature: 20_000)\n\n# Will use an initial temperature of 10,000 and a cooling rate of 0.003\nsimulation.run(locations.shuffle, cooling_rate: 0.003)\n```\n\n## Development\n\nAfter 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.\n\nTo 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).\n\n## Contributing\n\nBug 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).\n\n## Code of Conduct\n\nEveryone interacting in the Annealing project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/3zcurdia/annealing/blob/master/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F3zcurdia%2Fannealing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F3zcurdia%2Fannealing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F3zcurdia%2Fannealing/lists"}