-
Notifications
You must be signed in to change notification settings - Fork 6
/
solverl_test.exs
345 lines (292 loc) · 10.6 KB
/
solverl_test.exs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
defmodule SolverlTest do
use ExUnit.Case
doctest Solverl
import MinizincUtils
@nqueens_model resource_file("mzn/nqueens.mzn")
@sudoku_model resource_file("mzn/sudoku.mzn")
@aust_model resource_file("mzn/aust.mzn")
@test1_model resource_file("mzn/test1.mzn")
@test_data2 resource_file("mzn/test_data2.dzn")
test "Runs the proper solver CMD" do
test_arr = [[[1, 2, 3], [2, 3, 1], [3, 4, 5]], [[1, 2, 3], [2, 3, 1], [3, 4, 5]]]
assert {:ok, _pid} =
MinizincSolver.solve(
@test1_model,
[
%{
test_data1: 100,
test_arr: test_arr,
test_base_arr: {[0, 1, 0], test_arr},
test_set: MapSet.new([1, 2, 3]),
test_enum: MapSet.new([:red, :blue, :white]),
test_enum_arr: {["test_enum"], [1, 2, 3]},
test_enum_arr2d:
{["test_enum", "test_enum"],
[
[1, 2, 3],
[4, 5, 6]
]}
},
@test_data2
],
solver: "gecode"
)
end
test "The same as above, but with multiple models either as a text or a file" do
test_arr = [[[1, 2, 3], [2, 3, 1], [3, 4, 5]], [[1, 2, 3], [2, 3, 1], [3, 4, 5]]]
models = [{:model_text, "int: test_model = true;"}, @test1_model]
assert {:ok, _pid} =
MinizincSolver.solve(
models,
[
%{
test_data1: 100,
test_arr: test_arr,
test_base_arr: {[0, 1, 0], test_arr},
test_set: MapSet.new([1, 2, 3]),
test_enum: MapSet.new([:red, :blue, :white]),
test_enum_arr: {["test_enum"], [1, 2, 3]},
test_enum_arr2d:
{["test_enum", "test_enum"],
[
[1, 2, 3],
[4, 5, 6]
]}
},
@test_data2
],
solver: "gecode"
)
end
test "Minizinc error" do
## Unrecognized Minizinc option
{:error, invalid_option_or_bad_format} =
MinizincSolver.solve_sync(@nqueens_model, %{n: 2}, extra_flags: "--fake-flag")
assert String.starts_with?(
invalid_option_or_bad_format,
"minizinc: Unrecognized option or bad format `--fake-flag'"
)
end
test "Checking dzn against the model: undefined identifier" do
## The model expects to have 'n' par as input, but gets 'm' par instead.
dzn = %{m: 4}
{:error, error} = MinizincModel.mzn_dzn_info(@nqueens_model, dzn)
assert String.contains?(error, "type error: undefined identifier `m'")
end
test "Checking dzn against the model: unassigned parameter" do
## Add new par description to the model
par_descr = "int: k;"
model_info = MinizincModel.mzn_dzn_info([@nqueens_model, {:model_text, par_descr}], %{n: 4})
assert MinizincData.check_dzn(model_info) == {:error, {:unassigned_pars, MapSet.new(["k"])}}
end
test "Unsatisfiable sync" do
{:ok, unsat_res} = MinizincSolver.solve_sync(@nqueens_model, %{n: 2})
assert MinizincResults.get_status(unsat_res) == :unsatisfiable
end
test "Solving with timeout sync" do
## Final record for sync solving results is in position 0.
{:ok, final_data} = MinizincSolver.solve_sync(@nqueens_model, %{n: 50}, time_limit: 500)
assert MinizincResults.get_status(final_data) == :satisfied
end
test "Getting all solutions" do
{:ok, results} = MinizincSolver.solve_sync(@nqueens_model, %{n: 8})
assert MinizincResults.get_status(results) == :all_solutions
## 92 results for the nqueens.mzn model.
assert length(results[:solutions]) == 92
end
test "Sync solving: solution handler that interrupts the solver after first 100 solutions have been found" do
{:ok, final_data} =
MinizincSolver.solve_sync(
@nqueens_model,
%{n: 50},
solution_handler: MinizincSearch.find_k_handler(100, MinizincHandler.Default)
)
assert length(MinizincResults.get_solutions(final_data)) == 100
assert MinizincResults.get_status(final_data) == :satisfied
end
test "Sync solving: solution handler that skips every other solution" do
{:ok, results} =
MinizincSolver.solve_sync(
@nqueens_model,
%{n: 8},
solution_handler: SolverTest.EveryOther
)
## 92 results for the nqueens.mzn model, but we drop every other one...
assert length(MinizincResults.get_solutions(results)) == div(92, 2)
end
test "Sync solving: solution handler throws an exception" do
{:ok, results} =
MinizincSolver.solve_sync(
@nqueens_model,
%{n: 9},
solution_handler: SolverTest.ThrowAfter100
)
## Should get 100 solutions
assert length(MinizincResults.get_solutions(results)) == 100
## The exception is stored with :handler_exception key
assert results[:handler_exception] == :throw_after_100
end
test "Checks dimensions of a regular array " do
good_arr = [[[1, 2, 3], [2, 3, 1], [3, 4, 5]], [[1, 2, 3], [2, 3, 1], [3, 4, 5]]]
assert MinizincData.dimensions(good_arr) == [2, 3, 3]
end
test "Throws exception if the array is not regular" do
bad_array = [1, [2, 3]]
data = %{bad_arr: bad_array}
assert catch_throw(MinizincData.to_dzn(data)) == {:irregular_array, bad_array}
end
test "Model with boolean vars" do
conjunction_model = "var bool: x;\nvar bool: y;\nconstraint x /\\ y;\n"
{:ok, results} = MinizincSolver.solve_sync({:model_text, conjunction_model})
solution = Enum.at(MinizincResults.get_solutions(results), 0)
assert MinizincResults.get_solution_value(solution, "x") and
MinizincResults.get_solution_value(solution, "y") == true
end
test "Model with 2d array of vars" do
array_model = """
array[1..4, 1..5] of var 0..1: arr;
constraint forall(i in 1..4)(
forall(j in 1..4)(
arr[i, j] != arr[i, j+1]
)
);
"""
{:ok, results} = MinizincSolver.solve_sync({:model_text, array_model})
assert MinizincResults.get_last_solution(results)
|> MinizincResults.get_solution_value("arr")
|> MinizincData.dimensions() == [4, 5]
end
test "Model with set vars" do
set_model = """
var set of 0..5: var_set;
constraint 1 in var_set;
constraint 2 in var_set;
constraint card(var_set) == 2;
"""
{:ok, results} = MinizincSolver.solve_sync({:model_text, set_model})
solution = Enum.at(MinizincResults.get_solutions(results), 0)
assert MinizincResults.get_solution_value(solution, "var_set") == MapSet.new([1, 2])
end
test "Model with enums" do
enum_model = """
enum COLOR;
COLOR: some_color;
var COLOR: bigger_color;
constraint bigger_color > some_color;
"""
{:ok, results} =
MinizincSolver.solve_sync(
{:model_text, enum_model},
%{
COLOR: {"White", "Black", "Red", "Blue", "Green"},
some_color: "Blue"
}
)
solution = Enum.at(MinizincResults.get_solutions(results), 0)
assert MinizincResults.get_solution_value(solution, "bigger_color") == "Green"
end
test "Get last solution from summary, drop intermediate solutions" do
{:ok, results} =
MinizincSolver.solve_sync(
@nqueens_model,
%{n: 8},
solution_handler: SolverTest.SummaryOnly
)
## We dropped all solutions...
assert length(MinizincResults.get_solutions(results)) == 0
## ... but the solution count is still correct...
last_solution = MinizincResults.get_last_solution(results)
assert MinizincResults.get_solution_index(last_solution) == 92
## ... and the last solution is there
assert length(MinizincResults.get_solution_value(last_solution, "q")) == 8
end
test "Fix decision variable (used for LNS)" do
## Randomly destruct values of "var1" variable by 50%
assert MinizincSearch.destroy_var("var1", [1, 2], 0.5, 2) in [
"constraint var1[2] = 1;\n",
"constraint var1[3] = 2;\n"
]
end
test "Get model info" do
model_info = MinizincModel.model_info(@sudoku_model)
## Model has "start" parameter
assert model_info[:pars]["start"] == %{"dim" => 2, "type" => "int"}
## Model has "puzzle" variable
assert model_info[:vars]["puzzle"] == %{"dim" => 2, "type" => "int"}
end
test "Run model with checker" do
{:ok, results} =
MinizincSolver.solve_sync(
@aust_model,
nil,
checker: resource_file("mzn/aust.mzc.mzn")
)
assert String.trim(
MinizincResults.get_checker_output(MinizincResults.get_last_solution(results))
) == "CORRECT"
end
test "Shut down on 'no new solution' timeout" do
## Give it a very little time to wait for a solution...
{:ok, results} = MinizincSolver.solve_sync(@aust_model, nil, solution_timeout: 1)
## No solutions...
## ...but it did compile...
## ...and the exit reason indicates a solution timeout
assert results[:summary][:compiled]
assert results[:summary][:exit_reason] == :by_solution_timeout
end
test "Shut down on compilation timeout" do
## Give it a very little time to compile...
{:ok, results} = MinizincSolver.solve_sync(@aust_model, nil, fzn_timeout: 10)
## No solutions...
## ...and it didn't compile...
## ...and the exit reason indicates a FZN timeout
assert not MinizincResults.has_solution(results) and
not results[:summary][:compiled] and
results[:summary][:exit_reason] == :by_fzn_timeout
end
test "can create an array of sets" do
array = [
MapSet.new([1, 2]),
MapSet.new([3]),
MapSet.new()
]
expected = "array1d(1..3,[{1, 2}, {3}, {}])"
actual = MinizincData.elixir_to_dzn(array)
assert expected == actual
end
end
####################
## Helper modules ##
####################
defmodule SolverTest.EveryOther do
use MinizincHandler
@doc false
def handle_solution(%{index: count, data: data}) do
if rem(count, 2) == 0 do
:skip
else
data
end
end
@doc false
def handle_summary(summary) do
summary
end
end
defmodule SolverTest.SummaryOnly do
use MinizincHandler
@doc false
def handle_solution(_solution) do
:skip
end
end
## For testing throws within a solution handler
#
defmodule SolverTest.ThrowAfter100 do
use MinizincHandler
@doc false
def handle_solution(%{index: index, data: data} = _solution) do
if index > 100, do: throw(:throw_after_100)
data
end
end