Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AllDifferent FWC: refactoring #54

Merged
merged 5 commits into from
Sep 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/solver/constraints/circuit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ defmodule CPSolver.Constraint.Circuit do

@impl true
def propagators(x) do
[CircuitPropagator.new(x), AllDifferentPropagator.new(x)]
[CircuitPropagator.new(x)]
end
end
205 changes: 62 additions & 143 deletions lib/solver/constraints/propagators/all_different_fwc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,6 @@ defmodule CPSolver.Propagator.AllDifferent.FWC do
The forward-checking propagator for AllDifferent constraint.
"""

@impl true
def reset(args, nil, _opts) do
{initial_unfixed_vars, initial_fixed_values} = initial_reduction(args)
%{unfixed_vars: initial_unfixed_vars, fixed_values: initial_fixed_values}
end

def reset(args, %{fixed_values: fixed_values, unfixed_vars: unfixed_vars} = _state, _opts) do
{unfixed_vars, delta, total_fixed} =
Enum.reduce(
unfixed_vars,
{unfixed_vars, MapSet.new(), fixed_values},
fn idx,
{unfixed_acc, delta_acc, total_fixed_acc} =
acc ->
case get_value(args, idx) do
nil ->
acc

value ->
{MapSet.delete(unfixed_acc, idx), add_fixed_value(delta_acc, value),
add_fixed_value(total_fixed_acc, value)}
end
end
)

{final_unfixed_vars, final_fixed_values} = fwc(args, unfixed_vars, delta, total_fixed)
%{unfixed_vars: final_unfixed_vars, fixed_values: final_fixed_values}
end

defp initial_reduction(args) do
Arrays.reduce(
args,
{0, {MapSet.new(), MapSet.new()}},
fn var, {idx_acc, {unfixed_map_acc, fixed_set_acc}} ->
{idx_acc + 1,
(fixed?(var) && {unfixed_map_acc, add_fixed_value(fixed_set_acc, min(var))}) ||
{MapSet.put(unfixed_map_acc, idx_acc), fixed_set_acc}}
end
)
|> elem(1)
|> then(fn {unfixed_vars, fixed_values} ->
fwc(args, unfixed_vars, fixed_values, fixed_values)
end)
end

@impl true
def arguments(args) do
Arrays.new(args, implementation: Aja.Vector)
Expand All @@ -62,124 +17,88 @@ defmodule CPSolver.Propagator.AllDifferent.FWC do

@impl true
def filter(all_vars, state, changes) do
{unfixed_vars, fixed_values} =
if state do
{state.unfixed_vars, state.fixed_values}
else
initial_reduction(all_vars)
end
new_fixed = Map.keys(changes) |> MapSet.new()

{updated_unfixed_vars, updated_fixed_values} =
filter_impl(all_vars, unfixed_vars, fixed_values, changes)
{unresolved, fixed} =
(state &&
{state[:unresolved] |> MapSet.difference(new_fixed), fixed_values(all_vars, new_fixed)}) ||
initial_split(all_vars)

{:state, %{unfixed_vars: updated_unfixed_vars, fixed_values: updated_fixed_values}}
case fwc(all_vars, unresolved, fixed) do
false -> :passive
unfixed_updated_set -> {:state, %{unresolved: unfixed_updated_set}}
end
end

defp filter_impl(all_vars, unfixed_vars, fixed_values, changes) when is_map(changes) do
{new_unfixed_vars, new_fixed_values, all_fixed_values} =
prepare_changes(all_vars, unfixed_vars, fixed_values, changes)

fwc(all_vars, new_unfixed_vars, new_fixed_values, all_fixed_values)
defp fixed_values(vars, fixed) do
Enum.reduce(fixed, MapSet.new(), fn idx, values_acc ->
val = Propagator.arg_at(vars, idx) |> min()
(val in values_acc && fail()) || MapSet.put(values_acc, val)
end)
end

defp prepare_changes(all_vars, unfixed_vars, previously_fixed_values, changes) do
Enum.reduce(
changes,
{unfixed_vars, MapSet.new(), previously_fixed_values},
fn {idx, :fixed}, {unfixed_vars_acc, fixed_values_acc, all_fixed_values_acc} = acc ->
if MapSet.member?(unfixed_vars_acc, idx) do
updated_vars = MapSet.delete(unfixed_vars_acc, idx)
fixed_value = get_value(all_vars, idx)

{updated_vars, add_fixed_value(fixed_values_acc, fixed_value),
add_fixed_value(all_fixed_values_acc, fixed_value)}
else
acc
end
defp initial_split(vars) do
Enum.reduce(0..(Arrays.size(vars) - 1), {MapSet.new(), MapSet.new()}, fn idx,
{unfixed_acc,
fixed_vals_acc} ->
var = Propagator.arg_at(vars, idx)

if fixed?(var) do
val = min(var)
(val in fixed_vals_acc && fail()) || {unfixed_acc, MapSet.put(fixed_vals_acc, val)}
else
{MapSet.put(unfixed_acc, idx), fixed_vals_acc}
end
)
end)
end

defp fwc(all_vars, unfixed_vars, current_delta, accumulated_fixed_values) do
{updated_unfixed_vars, _fixed_values, new_delta} =
Enum.reduce(
unfixed_vars,
{unfixed_vars, current_delta, MapSet.new()},
fn idx, {unfixed_vars_acc, fixed_values_acc, new_delta_acc} ->
case remove_all(get_variable(all_vars, idx), fixed_values_acc) do
## No new fixed variables
false ->
{unfixed_vars_acc, fixed_values_acc, new_delta_acc}

new_fixed_value ->
{MapSet.delete(unfixed_vars_acc, idx),
MapSet.put(fixed_values_acc, new_fixed_value),
MapSet.put(new_delta_acc, new_fixed_value)}
end
end
)

updated_accumulated_fixed_values = MapSet.union(accumulated_fixed_values, new_delta)

if MapSet.size(new_delta) == 0 do
{updated_unfixed_vars, updated_accumulated_fixed_values}
else
fwc(all_vars, updated_unfixed_vars, new_delta, updated_accumulated_fixed_values)
end

##
defp fwc(vars, unfixed_set, fixed_values) do
{updated_unfixed, _fixed_vals} = remove_values(vars, unfixed_set, fixed_values)
MapSet.size(updated_unfixed) > 1 && updated_unfixed
end

## Remove values from the domain of variable
## Note: if the variable gets fixed at some point,
## we can stop by checking if the fixed value is already present in the set of values.
## If that's the case, we'll fail (duplicate fixed value!),
## otherwise we exit the loop, as there is no point to continue.
defp remove_all(nil, _values) do
false
end
## unfixed_set - set of indices for yet unfixed variables
## fixed_values - the set of fixed values we will use to reduce unfixed set.
defp remove_values(vars, unfixed_set, fixed_values) do
for idx <- unfixed_set, reduce: {MapSet.new(), fixed_values} do
{still_unfixed_acc, fixed_vals_acc} ->
var = Propagator.arg_at(vars, idx)

defp remove_all(variable, values) do
Enum.reduce_while(
values,
false,
fn value, _acc ->
case remove(variable, value) do
:fixed ->
fixed_value = min(variable)
(MapSet.member?(values, fixed_value) && throw(:fail)) || {:halt, fixed_value}

_not_fixed ->
{:cont, false}
end
end
)
end
case remove_all(var, fixed_vals_acc) do
false ->
## Variable is still unfixed, keep it
{MapSet.put(still_unfixed_acc, idx), fixed_vals_acc}

defp add_fixed_value(fixed_values, nil) do
fixed_values
end
new_fixed_value ->
fixed_vals_acc = MapSet.put(fixed_vals_acc, new_fixed_value)

defp add_fixed_value(fixed_values, value) do
(MapSet.member?(fixed_values, value) && throw(:fail)) ||
MapSet.put(fixed_values, value)
end
{unfixed_here, fixed_here} =
remove_values(vars, still_unfixed_acc, MapSet.new([new_fixed_value]))

defp get_value(_variables, nil) do
nil
{unfixed_here, MapSet.union(fixed_here, fixed_vals_acc)}
end
end
end

defp get_value(variables, idx) do
case get_variable(variables, idx) do
nil ->
nil
defp remove_all(var, values) do
Enum.reduce_while(values, false, fn val, acc ->
if remove(var, val) == :fixed do
{:halt, :fixed}
else
{:cont, acc}
end
end)
|> case do
false ->
fixed?(var) && min(var)

var ->
(fixed?(var) && min(var)) || nil
:fixed ->
min(var)
end
|> then(fn new_min -> new_min && ((new_min in values && fail()) || new_min) end)
end

defp get_variable(variables, idx) do
(idx && Propagator.arg_at(variables, idx)) || nil
defp fail() do
throw(:fail)
end
end
2 changes: 1 addition & 1 deletion test/constraints/all_different_fwc_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule CPSolverTest.Constraint.AllDifferent.FWC do
test "all fixed" do
variables = Enum.map(1..5, fn i -> IntVariable.new(i) end)
model = Model.new(variables, [Constraint.new(AllDifferentFWC, variables)])
{:ok, result} = CPSolver.solve_sync(model, timeout: 100)
{:ok, result} = CPSolver.solve_sync(model)

assert hd(result.solutions) == [1, 2, 3, 4, 5]
assert result.statistics.solution_count == 1
Expand Down