Solverl
Erlang/Elixir interface to Minizinc.
Inspired by Minizinc Python.
Disclaimer: This project is in its very early stages, and has not been used in production, nor extensively tested. Use at your own risk.
Installation
You will need to install Minizinc. Please refer to https://www.minizinc.org/software.html for details.
Note:
The code was only tested and run on macOS Catalina and Ubuntu 18.04 with Minizinc v2.4.3.
Note:
minizinc
executable is expected to be in its default location, or in a folder in the $PATH env
variable.
Otherwise, you can use minizinc_executable
option (see Solver Options).
The package can be installed by adding solverl
to your list of dependencies in mix.exs
:
def deps do
[
{:solverl, "~> 0.1.3"}
]
end
The docs can be found at https://hexdocs.pm/solverl.
Features
- Synchronous and asynchronous solving
- Pluggable solution handlers
- Support for basic Minizinc types, arrays, sets and enums
Usage
Minizinc module provides functions both for synchronous and asynchronous solving.
# Asynchronous solving.
# Creates a solver process.
{:ok, solver_pid} = Minizinc.solve(model, data, opts)
# Synchronous solving.
# Starts the solver and gets the results (solutions and/or solver stats) once the solver finishes.
solver_results = Minizinc.solve_sync(model, data, opts)
, where
model
- specification of Minizinc model;data
- specification of data passed tomodel
;opts
- solver options.
Model specification
Model could be either:
a string, in which case it represents a path for a file containing Minizinc model.
Example: "mzn/sudoku.mzn"
or, a tuple {:text,
model_text
}.Example (model as a multiline string):
""" array [1..5] of var 1..n: x; include "alldifferent.mzn"; constraint alldifferent(x); """
or a (mixed) list of the above. The code will build a model by concatenating bodies of model files and model texts suffixed with EOL (\n).
Example:
["mzn/test1.mzn", {:text, "constraint y[1] + y[2] <= 0;"}]
Data specification
Data could be either:
a string, in which case it represents a path for a Minizinc data file.
Example: "mzn/sudoku.dzn"
a map, in which case map keys/value represent model
par
names/values.Example:
%{n: 5, f: 3.44}
or a (mixed) list of the above. The code will build a data file by mapping elements of the list to bodies of data files and/or data maps, serialized as described in Support for Minizinc data types, then concatenating the elements of the list, suffixed with EOL (\n).
Example:
["mzn/test_data1.dzn", "mzn/test_data2.dzn", %{x: 2, y: -3, z: true}]
Support for Minizinc data types
Arrays
Minizinc
array
type corresponds to (nested) List. The code determines dimensions of the array based on its nested structure. By default, the indices of the dimensions are 1-based. Example:arr2d = [ [0, 1, 0, 1, 0], [0, 1, 0, 1, 0], [0, 1, 0, 1, 0], [0, 1, 0, 1, 0], [0, 1, 0, 1, 0] ] MinizincData.elixir_to_dzn(arr2d)
Output:
"array2d(1..5,1..5,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0])"
You can explicitly specify bases for each dimension:
# Let 1st dimension be 0-based, 2nd dimension be 1-based MinizincData.elixir_to_dzn({[0, 1], arr2d})
Output:
"array2d(0..4,1..5,[0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0,0,1,0,1,0])"
Sets
Minizinc
set
type corresponds to MapSet. Example:MinizincData.elixir_to_dzn(MapSet.new([2, 1, 6]))
Output:
"{1,2,6}"
Enums
Minizinc
enum
type corresponds to Tuple. Tuple elements have to be either of strings, charlists or atoms. Example 1 (using strings, atoms and charlists for enum entries):MinizincData.elixir_to_dzn({"blue", :BLACK, 'GREEN'})
Output:
"{blue, BLACK, GREEN}"
Example 2 (solving for
enum
variable):enum_model = """ enum COLOR; var COLOR: color; constraint color = max(COLOR); """ results = Minizinc.solve_sync({:text, enum_model}, %{'COLOR': {"White", "Black", "Red", "BLue", "Green"}}) results[:summary][:last_solution][:data]["color"]
Output:
"Green"
Solver options
solver
: Solver id supported by your Minizinc configuration. Default: "gecode".time_limit
: Time in msecs given to Minizinc to find a solution. Default: 300000 (5 mins).minizinc_executable
: Full path to Minizinc executable (you'd need it if executable cannot be located by your system).solution_handler
: Module or function that controls processing of solutions and/or metadata. Default: MinizincHandler.DefaultAsync.Check out Solution handlers for more details.
extra_flags
: A string of command line flags supported by the solver.
Example:
## Solve "mzn/nqueens.mzn" for n = 4, using Gecode solver,
## time limit of 1 sec, NQueens.SyncHandler as a solution handler.
## Extra flags: -O4 --verbose-compilation
Minizinc.solve_sync("mzn/nqueens.mzn", %{n: 4},
[solver: "gecode",
time_limit: 1000,
solution_handler: NQueens.SyncHandler,
extra_flags: "-O4 --verbose-compilation"])
Solution handlers
Solution handler
is a pluggable code created by the user in order to customize
handling of solutions and metadata produced by Minizinc.solve/3 and Minizinc.solve_sync/3.
Solution handler is either
- a function, or
- a module that implements MinizincHandler behaviour.
It represents a pluggable logic to customize processing of the solutions and metadata produced by Minizinc solver process.
Solution handler is specified by solution_handler
option.
Solution handler code acts as a callback for the solver events emitted by MinizincPort, which is a wrapper process for Minizinc executable (see Under the hood for more details).
Currently, there are following types of solver events:
solution
- the new solution detected;summary
- the wrapper sent the summary metadata (usually because the solver had finished);minizinc_error
- the wrapper detected Minizinc runtime error.
In case the solution handler is a function, its signature has to have 2 arguments, 1st one is an atom being the event name(i.e., :solution, :summary, :minizinc_error), 2nd being the event-specific data of that event.
In case the solution handler is a module that implements MinizincHandler behaviour,
its functions handle_solution/1
, handle_summary/1
, handle_minizinc_error/1
take an event-specific data.
Event-specific data.
For
:solution
event:%{ data: data, # Map of values keyed with their variable names timestamp: timestamp, # Timestamp of the moment solution was parsed index: index, # Sequential number of the solution stats: stats # Map of solution statistics values keyed with the field names }
For
:summary
event:%{ status: status, # Solver status (one of :satisfied, :unsatisfiable etc) fzn_stats: fzn_stats, # Map of FlatZinc statistics values keyed with the field names solver_stats: solver_stats, # Map of solver statistics values keyed with the field names solution_count: solution_count, # Total number of solutions found last_solution: solution, # Data for last :solution event (see above) minizinc_output: minizinc_output, # Minizinc errors and warnings time_elapsed: time_elapsed # Time elapsed, verbatim as reported by Minizinc }
For
:minizinc_error
event:%{ error: error # Minizinc output accompanied by runtime error }
Tailoring results and controlling execution.
Solution handlers can modify or ignore data passed by solver events, or interrupt the solver process early, by constructing their returns in desired form.
The return of the solution handler callback could be one of:
:stop
Solution handler stops receiving solver events and asks solver to stop execution.
{:stop, data}
Same as above, but in case of synchronous solving,
data
will be added to solver results.:skip
The event will be ignored, i.e. in case of synchronous solving, the results will not be changed.
data :: any()
In case of synchronous solving, data will be added to solver results.
Solver results
Note: this is applicable only to a synchronous solving.
Solver results
is a map with the following keys:
:solutions
- list of data elements, accumulated by handling of:solution
events:summary
- data, produced by handling of:summary
event:minizinc_error
- data, produced by handling of:minizinc_error
event
Please refer to Event-specific data.
Handling exceptions
Solution handler is a pluggable code that is typically created by a user. MinizincSolver catches exceptions from solution handlers
to make sure that the solver process is gracefully shut down. Moreover, in case of synchronous solving,
MinizincSolver preserves the solver results accumulated before the exception, and returns them to the calling process.
The exception value will be added to solver results under :handler_exception
key.
Examples
N-Queens
The following code solves N-queens puzzle for N = 4:
NQueens.solve(4, [solution_handler: &NQueens.solution_handler/2])
Output:
17:16:53.073 [warn] Command: /Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s -a --solver org.gecode.gecode --time-limit 300000 /var/folders/rn/_39sx1c12ws1x5k66n_cjjh00000gn/T/tmp.vFlJER37.mzn /var/folders/rn/_39sx1c12ws1x5k66n_cjjh00000gn/T/tmp.rxTZq96j.dzn
{:ok, #PID<0.766.0>}
iex(75)>
17:16:53.174 [info]
. . ♕ .
♕ . . .
. . . ♕
. ♕ . .
-----------------------
17:16:53.174 [info]
. ♕ . .
. . . ♕
♕ . . .
. . ♕ .
-----------------------
17:16:53.179 [info] Solution status: all_solutions
17:16:53.179 [info] Solver stats:
%{failures: 4, initTime: 0.007719, nSolutions: 2, nodes: 11, peakDepth: 2, propagations: 163, propagators: 11, restarts: 0, solutions: 2, solveTime: 0.002778, variables: 12}
17:16:53.179 [debug] ** TERMINATE: :normal
Sudoku
Sudoku.solve("85...24..72......9..4.........1.7..23.5...9...4...........8..7..17..........36.4.")
The output:
17:19:28.109 [info] Sudoku puzzle:
17:19:28.109 [info]
+-------+-------+-------+
| 8 5 . | . . 2 | 4 . . |
| 7 2 . | . . . | . . 9 |
| . . 4 | . . . | . . . |
+-------+-------+-------+
| . . . | 1 . 7 | . . 2 |
| 3 . 5 | . . . | 9 . . |
| . 4 . | . . . | . . . |
+-------+-------+-------+
| . . . | . 8 . | . 7 . |
| . 1 7 | . . . | . . . |
| . . . | . 3 6 | . 4 . |
+-------+-------+-------+
17:19:28.154 [warn] Command: /Applications/MiniZincIDE.app/Contents/Resources/minizinc --allow-multiple-assignments --output-mode json --output-time --output-objective --output-output-item -s -a --solver org.gecode.gecode --time-limit 300000 /var/folders/rn/_39sx1c12ws1x5k66n_cjjh00000gn/T/tmp.uSF45sHN.mzn /var/folders/rn/_39sx1c12ws1x5k66n_cjjh00000gn/T/tmp.KjyUqmEa.dzn
{:ok, #PID<0.776.0>}
iex(79)>
17:19:28.219 [info]
+-------+-------+-------+
| 8 5 9 | 6 1 2 | 4 3 7 |
| 7 2 3 | 8 5 4 | 1 6 9 |
| 1 6 4 | 3 7 9 | 5 2 8 |
+-------+-------+-------+
| 9 8 6 | 1 4 7 | 3 5 2 |
| 3 7 5 | 2 6 8 | 9 1 4 |
| 2 4 1 | 5 9 3 | 7 8 6 |
+-------+-------+-------+
| 4 3 2 | 9 8 1 | 6 7 5 |
| 6 1 7 | 4 2 5 | 8 9 3 |
| 5 9 8 | 7 3 6 | 2 4 1 |
+-------+-------+-------+
17:19:28.219 [info] Solutions found: 1
17:19:28.224 [info] Status: all_solutions
17:19:28.224 [info] Solver statistics:
%{failures: 11, initTime: 0.001401, nSolutions: 1, nodes: 23, peakDepth: 5, propagations: 685, propagators: 27, restarts: 0, solutions: 1, solveTime: 0.001104, variables: 147}
17:19:28.224 [debug] ** TERMINATE: :normal
Erlang interface
minizinc
module mirrors all exported functions of Elixir Minizinc module.
Once you manage to make solverl
dependency part of your Erlang application build (for instance with rebar_mix),
you should be able to use its interface.
Example:
results = minizinc:solve_sync(<<"mzn/nqueens.mzn">>, #{n=> 4}, [{solution_handler, fun 'Elixir.NQueens':solution_handler/2}]).
Note: Please use binary
strings as opposed to char
strings whenever you need to pass a string to API.
The API functions will always use binary
strings whenever the function return needs to use strings.
Under the hood
Both Minizinc.solve/3 and Minizinc.solve_sync/3 use MinizincPort.start_link/3 to start GenServer process, which in turn spawns the external MiniZinc process, and then parses its text output into solver events and makes appropriate callback function calls as described here.
Roadmap
Support LNS;
Support Branch-and-Bound;
Provide API for peeking into a state of Minizinc process, such as time since last solution,
whether it's compiling or solving at the moment etc.
Match minizinc-python functionality.
Credits
TODO