defmodule CPSolverTest.Search.AFC do use ExUnit.Case alias CPSolver.Search.VariableSelector.AFC, as: AFC alias CPSolver.Space import CPSolver.Test.Helpers describe "AFC search strategy" do alias CPSolver.Shared test "initialization from top space" do space = build_space() shared = Space.get_shared(space) decay = :rand.uniform() AFC.initialize(space, decay) assert decay == AFC.get_decay(shared) afc_table_ref = AFC.get_afc_table(shared) propagator_refs = Enum.map(space.propagators, fn p -> p.id end) assert Enum.all?( :ets.tab2list(afc_table_ref), fn {p_ref, {1, 0}} -> p_ref in propagator_refs end ) end test "get AFC record for propagator" do space = build_space() AFC.initialize(space, :rand.uniform()) propagator_refs = Enum.map(space.propagators, fn p -> p.id end) shared = Space.get_shared(space) assert Enum.all?(propagator_refs, fn p_id -> {1, 0} = AFC.get_afc_record(p_id, shared) end) end test "update propagator AFC" do space = build_space() shared = Space.get_shared(space) decay = :rand.uniform() AFC.initialize(space, decay) [p1, p2, p3 | _rest] = Enum.map(space.propagators, fn p -> p.id end) ## Simulate propagator failure for 'p1' propagator Shared.add_failure(shared, {:fail, p1}) {p1_afc, 1} = AFC.get_afc_record(p1, shared) assert p1_afc == 2 ## Manually decay p2 AFC.update_afc(p2, shared, false) ## AFC record advances to decayed value and the last global failure count assert {decay, 1} == AFC.get_afc_record(p2, shared) ## Simulate propagator failure for 'p2' propagator Shared.add_failure(shared, {:fail, p2}) ## Trigger update for 'p2' ## AFC record is incremented by 1, the last global failure count is updated assert {decay + 1, 2} == AFC.get_afc_record(p2, shared) ## Compute AFC for 'p1' again, it should be decayed once. assert {p1_afc * decay, 2} == AFC.propagator_afc(p1, shared) ## Compute AFC for 'p3'. It should be decayed twice from initial value of 1. assert {decay * decay, 2} == AFC.propagator_afc(p3, shared) end test "Calculate variable AFC" do space = build_space() shared = Space.get_shared(space) decay = :rand.uniform() AFC.initialize(space, decay) variables = space.variables ## The propagators in the space setup are: ## X != Y, X != Z, Y != Z ## ## That is, each variable participates in 2 propagators. ## 1. The initial AFC for every variable should be equal to 2. Enum.each(variables, fn var -> assert 2 == AFC.variable_afc(var, shared) end) [p1, _p2, _p3] = space.propagators ## 2. Fail the propagator x != y ## This should increment AFCs for variables x and y and decay AFC for variable z assert Enum.map(p1.args, fn arg -> arg.name end) == ["x", "y"] Shared.add_failure(shared, {:fail, p1.id}) [var1, var2, var3] = space.variables assert var1.name == "x" && var2.name == "y" && var3.name == "z" ## Variables `x` and `y` participate in 1 failed (x != y) and 1 non-failed propagator. ## Hence the AFC for both of these variables is (2 + decay) assert 2 + decay == AFC.variable_afc(var1, shared) assert 2 + decay == AFC.variable_afc(var2, shared) ## Both propagators variable `z` have not failed, both of them decayed. ## So AFC for `z` is (2 * decay) assert 2 * decay == AFC.variable_afc(var3, shared) end test "Calculate multiple variable AFCs" do space = build_space() shared = Space.get_shared(space) decay = :rand.uniform() AFC.initialize(space, decay) variables = space.variables ## The propagators in the space setup are: ## X != Y, X != Z, Y != Z ## ## That is, each variable participates in 2 propagators. ## 1. The initial AFC for every variable should be equal to 2. assert Enum.all?(AFC.variable_afcs(variables, shared), fn {var, afc_value} -> 2 == afc_value end) [p1, _p2, _p3] = space.propagators # ## 2. Fail the propagator x != y # ## This should increment AFCs for variables x and y and decay AFC for variable z assert Enum.map(p1.args, fn arg -> arg.name end) == ["x", "y"] Shared.add_failure(shared, {:fail, p1.id}) [{var1, afc1}, {var2, afc2}, {var3, afc3}] = AFC.variable_afcs(variables, shared) |> Enum.sort_by(fn {var, _afc} -> var.name end) assert var1.name == "x" && var2.name == "y" && var3.name == "z" # ## Variables `x` and `y` participate in 1 failed (x != y) and 1 non-failed propagator. # ## Hence the AFC for both of these variables is (2 + decay) assert 2 + decay == afc1 assert 2 + decay == afc2 # ## Both propagators variable `z` have not failed, both of them decayed. # ## So AFC for `z` is (2 * decay) assert 2 * decay == afc3 end defp build_space() do shared = Shared.init_shared_data(space_threads: 4) space_setup(0..5, 0..5, 0..5) |> Map.put(:opts, solver_data: shared) |> tap(fn space -> Space.put_shared(space, :initial_constraint_graph, space.constraint_graph) end) end end end