{"id":21564626,"url":"https://github.com/elixir-or-tools/exhort","last_synced_at":"2025-04-10T13:07:19.159Z","repository":{"id":38396554,"uuid":"438056144","full_name":"elixir-or-tools/exhort","owner":"elixir-or-tools","description":"Experimental Elixir interface to Google's OR Tools","archived":false,"fork":false,"pushed_at":"2022-06-05T18:02:39.000Z","size":171,"stargazers_count":10,"open_issues_count":1,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-06T09:43:51.856Z","etag":null,"topics":["elixir","operations-research"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elixir-or-tools.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null}},"created_at":"2021-12-13T23:31:09.000Z","updated_at":"2024-05-12T05:24:34.000Z","dependencies_parsed_at":"2022-09-01T09:20:12.957Z","dependency_job_id":null,"html_url":"https://github.com/elixir-or-tools/exhort","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-or-tools%2Fexhort","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-or-tools%2Fexhort/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-or-tools%2Fexhort/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elixir-or-tools%2Fexhort/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elixir-or-tools","download_url":"https://codeload.github.com/elixir-or-tools/exhort/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248224047,"owners_count":21068071,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["elixir","operations-research"],"created_at":"2024-11-24T10:16:36.035Z","updated_at":"2025-04-10T13:07:19.140Z","avatar_url":"https://github.com/elixir-or-tools.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Exhort\n\nBeseech the maths to answer.\n\n## Overview\n\nExhort is an idomatic Elixir interface to the [Google OR\nTools](https://developers.google.com/optimization).\n\nCurrently, there are C++ (native) Python, Java and C# interfaces to the Google\nOR tools.\n\nExhort is similar to the non-native interfaces to the tooling, but Exhort uses\n[NIFs](https://www.erlang.org/doc/tutorial/nif.html) instead of\n[SWIG](http://www.swig.org/) to interface with the native libarary.\n\nThe goal of Exhort is to provide an idomatic Elixir interface to the Google OR\nTools.\n\n## Setup\n\nBecause Exhort uses the Google OR tools, the first step is to install them on\nthe target system.\n\n### MacOS\n\nOn MacOS, ensure the [latest command line\ntools](https://developer.apple.com/download/all/) are installed.\n\n```sh\npkgutil --pkg-info=com.apple.pkg.CLTools_Executables\n```\n\nNext, install the `or-tools` package from Homebrew:\n\n```sh\nbrew install or-tools\n```\n\nThen leverage `asdf` for the required versions of Elixir and Elang:\n\n```sh\nasdf install\n```\n\nFinally, export the locations of Erlang and the OR Tools for your specific\nsystem:\n\n```sh\nexport ERLANG_HOME=$HOME/.asdf/installs/erlang/24.2.1\nexport ORTOOLS=$(brew --prefix)/lib/ortools\n```\n### Debian\n\nFollow the instructions\n[here](https://developers.google.com/optimization/install/cpp/linux) and install\nfrom the appropriate archive. You will likely want to install them in a\nreasonable place like `/usr/local/lib` and perhaps link them to a consistent\npath.\n\nFor example:\n\n```sh\nwget https://github.com/google/or-tools/releases/download/v9.2/or-tools_amd64_debian-11_v9.2.9972.tar.gz\ntar xf or-tools_amd64_debian-11_v9.2.9972.tar.gz -C /usr/local/lib\nln -s /usr/local/lib/or-tools_Debian-11-64bit_v9.2.9972 /usr/local/lib/ortools\n```\n\nThen export the locations of Erlang and the OR Tools:\n\n```sh\nexport ERLANG_HOME=/usr/local/lib/erlang\nexport ORTOOLS=/usr/local/lib/ortools\n```\n\n### Compiling\n\nExhort uses NIFs for interfacing with the Google OR tools. This means that\nExhort NIFs must be compiled using a C compiler and Make. The `Makefile`\ncontains these instructions. It just needs to know where you have installed both\nErlang and the Google OR Tools. It will use the environment variables you\nexported above.\n\n```sh\nmix compile\nmix test\n```\n\n## Getting Started\n\nThe easiest way to get started is with the sample Livebook notebooks in the\n`notebooks` directory.\n\nStart [Livebook](https://livebook.dev/) and open a notebook (use whatever method\nyou like to start Livebook).\n\n```sh\n# export your `ERLANG_HOME` and `ORTOOLS` variables here\n$ mix escript.install hex livebook\n# if installed in `asdf` use `asdf reshim`\n$ pwd\n.../exhort\n$ livebook server --name livebook@127.0.0.1 --home .\n```\n\n1. Use the link that is written to the console and browse the `notebooks`\n   directory.\n2. Open a sample, maybe `multiple-knapsack.livemd` or `nurse-scheduling.livemd`,\n   since those have some visualizations.\n3. Run the cells in the notebook and inspect the results.\n\nThe notebooks are mostly implementations of some of the samples that come with\nthe Google OR Tools. That should provide a starting place for exploring the\nExhort API and expression language. There is more about the Exhort API and\nexpression language below, but the notebooks and tests are probably a good place\nto start.\n\n### As a dependency of a project\n\nAdd Exhort as a dependency to your project in the `mix.exs`:\n\n```elixir\n  {:exhort, \"~\u003e 0.1.0\"}\n```\n\n## API\n\nExhort is in the early stages of development. As such, we are investigating a\nvarity of API approaches. We may end up with more than one (a la Ecto), but in\nthe short term will likely focus on a single approach.\n\nThe API is centered around the `Builder` and `Expr` modules. Those modules\nleverage Elixir macros to provide a DSL \"expression language\" for Exhort.\n\n### Builder\n\nBuilding a model starts off with the `Builder`.\n\n`Builder` has functions for defining variables, specifying constraints and\ncreating a `%Model{}` using the `build` function.\n\nBy specifying `use Exhort.SAT.Builder`, all of the relevant modules will be\naliased and the Exhort macros will be expanded.\n\n```elixir\n  use Exhort.SAT.Builder\n  ...\n\n    builder =\n      Builder.new()\n      |\u003e Builder.def_int_var(\"x\", {0, 10})\n      |\u003e Builder.def_int_var(\"y\", {0, 10})\n      |\u003e Builder.def_bool_var(\"b\")\n      |\u003e Builder.constrain(\"x\" \u003e= 5, if: \"b\")\n      |\u003e Builder.constrain(\"x\" \u003c 5, unless: \"b\")\n      |\u003e Builder.constrain(\"x\" + \"y\" == 10, if: \"b\")\n      |\u003e Builder.constrain(\"y\" == 0, unless: \"b\")\n\n    {response, acc} =\n      builder\n      |\u003e Builder.build()\n      |\u003e Model.solve(fn\n        _response, nil -\u003e 1\n        _response, acc -\u003e acc + 1\n      end)\n\n    # 2 responses\n    acc |\u003e IO.inspect(label: \"acc: \")\n    response |\u003e IO.inspect(label: \"response: \")\n\n    # :optimal\n    response.status |\u003e IO.inspect(label: \"status: \")\n    # 10, 0, true\n    SolverResponse.int_val(response, \"x\") |\u003e IO.inspect(label: \"x: \")\n    SolverResponse.int_val(response, \"y\") |\u003e IO.inspect(label: \"y: \")\n    SolverResponse.bool_val(response, \"b\") |\u003e IO.inspect(label: \"b: \")\n```\n\nSee below for more about the expression language used in Exhort.\n\n### Expr\n\nSometimes it may be more convenient to build up expressions separately and then\nadd them to a `%Builer{}` all at once. This is often the case when more complex\ndata sets are invovled in generating many variables and constraints for the\nmodel.\n\nInstead of having to maintain the builder through an `Enum.reduce/3` construct\nlike this:\n\n```elixir\n    builder =\n      Enum.reduce(all_days, builder, fn day, builder -\u003e\n        Enum.reduce(all_shifts, builder, fn shift, builder -\u003e\n          shift_options = Enum.filter(shifts, fn {_n, d, s} -\u003e d == day and s == shift end)\n          shift_option_vars = Enum.map(shift_options, fn {n, d, s} -\u003e \"shift_#{n}_#{d}_#{s}\" end)\n\n          Builder.constrain(builder, sum(shift_option_vars) == 1)\n        end)\n      end)\n```\n\nExhort allows the generation of lists of variables or constraint, maybe using\n`Enum.map/2`:\n\n```elixir\n    shift_nurses_per_period =\n      Enum.map(all_days, fn day -\u003e\n        Enum.map(all_shifts, fn shift -\u003e\n          shift_options = Enum.filter(shifts, fn {_n, d, s} -\u003e d == day and s == shift end)\n          shift_option_vars = Enum.map(shift_options, fn {n, d, s} -\u003e \"shift_#{n}_#{d}_#{s}\" end)\n\n          Expr.new(sum(shift_option_vars) == 1)\n        end)\n      end)\n      |\u003e List.flatten()\n```\n\nThese may then be added to the builder as a list:\n\n```elixir\n    builder\n    |\u003e Builder.add(shift_nurses_per_period)\n...\n```\n\n### Variables\n\nModel variables in the expression language are symbolic, represented as strings\nor atoms, and so don't interfere to the surrounding Elixir context. This allows\nthe variables to be consistently referenced through a builder pipeline, for\nexample, without having to capture an intermediate result.\n\nElixir variables may be used \"as is\" in expressions, allowing variables to be\ngenerated from enumerable collections.\n\nIn the following expression, `\"x\"` is a model variable, while `y` is an Elixir\nvariable:\n\n```elixir\n\"x\" \u003c y + 3\n```\n\nVariables may be defined in a few ways. It's often convenient to just focus on\nthe `Expr` and `Builder` modules, which each have functions like `def_int_var`\nand `def_bool_var`.\n\n```elixir\n    all_bins\n    |\u003e Enum.map(fn bin -\u003e\n      Expr.def_bool_var(\"slack_#{bin}\")\n    end)\n```\n\nHowever, `BoolVar.new/1` and `IntVar.new/1` may also be used:\n\n```elixir\n    all_bins\n    |\u003e Enum.map(fn bin -\u003e\n      BoolVar.new(\"slack_#{bin}\")\n    end)\n```\n\nOf course, such names are still usable in expressions:\n\n```elixir\n    Expr.new(\"slack_#{bin}\" \u003c= bin_total)\n```\n\nNote that any variables or expressions created outside of the `Builder` still\nneed to be added to a `%Builder{}` struct for them to be part of the model\nresulting from `build/1`. There's no magic here, these are still Elixir\nimmutable data structures.\n\n```elixir\n    variables = ...\n    expressions = ...\n\n    Builder.new()\n    |\u003e Builder.add(variables)\n    |\u003e Builder.add(expressions)\n    |\u003e Builder.build()\n```\n\n### Expressions\n\nExhort supports a limited set of expressions. Expressions may use the binary\noperators `+`, `-` and `*`, with their traditional mathematical meaning. They\nmay also use comparison operators `\u003c`, `\u003c=`, `==`, `\u003e=`, `\u003e`, the `sum` function\nand even the `for` comprehension.\n\n```elixir\n    all_bins\n    |\u003e Enum.map(fn bin -\u003e\n      vars = Enum.map(items, \u0026{elem(\u00261, 0), \"x_#{elem(\u00261, 0)}_#{bin}\"})\n      load_bin = \"load_#{bin}\"\n\n      Expr.constrain(sum(for {item, x} \u003c- vars, do: item * x) == load_bin)\n    end)\n```\n\n### Model\n\nThe model is the result of finalizing the builder, created through the\n`Builder.build/1` function.\n\nThe model may then be solved with `Model.solve/1` or `Model.solve/2`.\n\nThe latter function allows for a function to be passed to receive intermediate\nsolutions from the solver.\n\n## Implementation\n\nExhort relies on the underlying native C++ implementation of the Google OR\nTools.\n\nExhort interacts with the Google OR Tools library when the model is built using\n`Builder.build/1` and when solved using `Model.solve/1` or `Model.solve/2`.\n\nReferences to the native objects are returned via NIF resources to the Elixir\nruntime as `%Reference{}` values. These are often stored in corresponding Exhort\nstructs under the `res` key.\n\nThe native code is compiled to a single `nif.so` library and loaded via the\n`Exhort.NIF.Nif` module.\n\n# Contributing\n\n1. Use clear descriptions in your commit message, both the header and the body.\n   Describe both what you did and why you did it.\n1. Make sure the tests run with your changes. Adding new tests for new\n   functionality is a good idea.\n1. Request reviews from the code owners.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-or-tools%2Fexhort","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felixir-or-tools%2Fexhort","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felixir-or-tools%2Fexhort/lists"}