Packages
fixpoint
0.4.1
0.22.1
0.21.5
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.18.2
0.18.1
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.9
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.13.5
0.13.4
0.13.2
0.13.1
0.12.9
0.12.8
0.12.7
0.12.6
0.12.5
0.12.4
0.12.2
0.12.1
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.46
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.28
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.3
0.4.2
0.4.1
0.4.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.1.3
0.1.2
0.1.1
0.1.0
Constraint Programming Solver
Current section
Files
Jump to
Current section
Files
lib/solver/core/space.ex
defmodule CPSolver.Space do
@moduledoc """
Computation space.
The concept is taken from Chapter 12, "Concepts, Techniques, and Models
of Computer Programming" by Peter Van Roy and Seif Haridi.
"""
alias CPSolver.Utils
alias __MODULE__, as: Space
alias CPSolver.ConstraintStore
alias CPSolver.Propagator
alias CPSolver.Propagator.Thread, as: PropagatorThread
alias CPSolver.Solution, as: Solution
alias CPSolver.IntVariable, as: Variable
alias CPSolver.DefaultDomain, as: Domain
alias CPSolver.Propagator.ConstraintGraph
alias CPSolver.Utils
require Logger
@behaviour :gen_statem
defstruct id: nil,
parent: nil,
keep_alive: false,
variables: [],
propagators: [],
constraint_graph: nil,
propagator_threads: %{},
store: nil,
space: nil,
solver: nil,
solution_handler: nil,
search: nil,
opts: []
defp default_space_opts() do
[
store: CPSolver.ConstraintStore.default_store(),
solution_handler: Solution.default_handler(),
search: CPSolver.Search.Strategy.default_strategy()
]
end
def create(variables, propagators, space_opts \\ [], gen_statem_opts \\ []) do
{:ok, _space} =
:gen_statem.start(
__MODULE__,
[
variables: variables,
propagators: propagators,
# Inject solver, if wasn't passed in opts
space_opts: inject_solver(space_opts)
],
gen_statem_opts
)
end
def stop(space) do
Process.alive?(space) && :gen_statem.stop(space)
end
defp inject_solver(space_opts) do
Keyword.put_new(space_opts, :solver, self())
end
def get_state_and_data(space) do
{_state, _data} = :sys.get_state(space)
end
def solution(%{variables: variables, store: store} = _data) do
Enum.reduce_while(variables, Map.new(), fn var, acc ->
case ConstraintStore.get(store, var, :min) do
:fail -> {:halt, :fail}
val -> {:cont, Map.put(acc, var.name, val)}
end
end)
end
@impl true
def init(args) do
variables = Keyword.get(args, :variables)
space_id = make_ref()
space_opts = Keyword.merge(default_space_opts(), Keyword.get(args, :space_opts, []))
store_impl = Keyword.get(space_opts, :store)
parent = Keyword.get(space_opts, :parent)
keep_alive = Keyword.get(space_opts, :keep_alive, false)
solution_handler = Keyword.get(space_opts, :solution_handler)
search_strategy = Keyword.get(space_opts, :search)
solver = Keyword.get(space_opts, :solver)
propagators = Keyword.get(args, :propagators) |> Propagator.normalize()
constraint_graph = create_constraint_graph(propagators)
{:ok, space_variables, store} =
ConstraintStore.create_store(variables,
store_impl: store_impl,
space: self(),
constraint_graph: constraint_graph
)
space_data = %Space{
id: space_id,
parent: parent,
keep_alive: keep_alive,
variables: space_variables,
propagators: propagators,
constraint_graph: constraint_graph,
store: store,
solver: solver,
opts: space_opts,
solution_handler: solution_handler,
search: search_strategy
}
{:ok, :start_propagation, space_data, [{:next_event, :internal, {:propagate, propagators}}]}
end
defp create_constraint_graph(propagators) do
propagators
|> ConstraintGraph.create()
|> then(fn graph ->
:ets.new(__MODULE__, [:set, :public, read_concurrency: true, write_concurrency: true])
|> tap(fn table_id -> ConstraintGraph.update_graph(graph, table_id) end)
# Temporary
# graph
end)
end
@impl true
def callback_mode() do
[:state_functions, :state_enter]
end
## Callbacks
def start_propagation(:enter, :start_propagation, data) do
{:keep_state, data}
end
def start_propagation(:internal, {:propagate, propagators}, data) do
propagator_threads = create_propagator_threads(propagators, data)
{:next_state, :propagating, Map.put(data, :propagator_threads, propagator_threads)}
end
def propagating(:enter, :start_propagation, _data) do
:keep_state_and_data
end
def propagating(:info, {:stable, propagator_thread}, data) do
updated_data = update_scheduled(data, propagator_thread, false)
if fixpoint?(updated_data) do
{:next_state, :stable, updated_data}
else
{:keep_state, updated_data}
end
end
def propagating(:info, {:entailed, propagator_thread}, data) do
updated_data = update_entailed(data, propagator_thread)
cond do
solved?(updated_data) -> {:next_state, :solved, updated_data}
fixpoint?(updated_data) -> {:next_state, :stable, updated_data}
true -> {:keep_state, updated_data}
end
end
def propagating(:info, {{domain_change, propagator_threads}, variable_id}, data) do
updated_data =
Enum.reduce(propagator_threads, data, fn p_ref, acc ->
notify_propagator(acc, p_ref, {domain_change, variable_id})
end)
{:keep_state, updated_data}
end
def propagating(:info, {:fail, _variable_id}, data) do
{:next_state, :failed, data}
end
def propagating(:info, :solved, data) do
{:next_state, :solved, data}
end
@spec failed(any, any, any) :: :keep_state_and_data
def failed(:enter, :propagating, data) do
handle_failure(data)
end
def failed(kind, message, _data) do
unexpected_message(:failed, kind, message)
end
def solved(:enter, :propagating, data) do
handle_solved(data)
end
def solved(kind, message, _data) do
unexpected_message(:solved, kind, message)
end
def stable(:enter, :propagating, data) do
handle_stable(data)
end
def stable(kind, message, _data) do
unexpected_message(:stable, kind, message)
end
defp unexpected_message(state, kind, message) do
Logger.error(
"Unexpected message in state #{inspect(state)}: #{inspect(kind)}: #{inspect(message)}"
)
:keep_state_and_data
end
defp create_propagator_threads(propagators, data) do
Map.new(propagators, fn {propagator_id, p} ->
{:ok, thread} =
PropagatorThread.create_thread(self(), p,
id: propagator_id,
store: data.store
)
{propagator_id, %{thread: thread, propagator: p, scheduled_runs: 1}}
end)
end
defp fixpoint?(%{propagator_threads: threads} = _data) do
Enum.all?(threads, fn {_id, thread} -> thread.scheduled_runs == 0 end)
end
defp update_scheduled(
%{propagator_threads: threads} = data,
propagator_id,
scheduled?,
thread_action \\ nil
) do
threads
|> Map.get(propagator_id)
|> then(fn
nil ->
data
%{scheduled_runs: scheduled_runs} = thread_rec ->
thread_action && thread_action.(thread_rec)
inc_dec = (scheduled? && 1) || -1
%{
data
| propagator_threads:
Map.put(
threads,
propagator_id,
Map.put(thread_rec, :scheduled_runs, scheduled_runs + inc_dec)
)
}
end)
end
defp notify_propagator(data, propagator_id, domain_change_event) do
update_scheduled(data, propagator_id, true, fn thread ->
send(thread.thread, domain_change_event)
end)
end
def update_entailed(
%{propagator_threads: threads, constraint_graph: graph} = data,
propagator_thread
) do
Map.put(
data,
:propagator_threads,
Map.delete(threads, propagator_thread)
)
|> tap(fn _data -> ConstraintGraph.remove_propagator(graph, propagator_thread) end)
end
defp solved?(data) do
map_size(data.propagator_threads) == 0
end
defp handle_failure(data) do
shutdown(data, :failure)
end
defp handle_solved(%{solution_handler: solution_handler} = data) do
data
|> solution()
|> then(fn
:fail ->
handle_failure(data)
solution ->
publish(data, {:solution, solution})
Solution.run_handler(solution, solution_handler)
shutdown(data, :solved)
end)
end
defp handle_stable(data) do
distribute(data)
end
def distribute(
%{
variables: variables
} = data
) do
{localized_vars, _all_fixed?} = Utils.localize_variables(variables)
do_distribute(data, localized_vars)
end
def do_distribute(
%{
propagator_threads: threads,
search: search_strategy
} = data,
variable_clones
) do
case branching(variable_clones, search_strategy) do
{:ok, {var_to_branch_on, domain_partitions}} ->
Enum.map(domain_partitions, fn partition ->
variable_copies =
Map.new(variable_clones, fn %{id: clone_id} = clone ->
if clone_id == var_to_branch_on.id do
{clone_id, Variable.copy(clone) |> Map.put(:domain, Domain.new(partition))}
else
{clone_id, Variable.copy(clone)}
end
end)
propagator_copies =
Enum.map(threads, fn {_ref, thread} ->
{propagator_mod, args} = thread.propagator
## Replace variables in args to their copies
{propagator_mod,
Enum.map(args, fn
%CPSolver.Variable{id: id} = _arg ->
Map.get(variable_copies, id)
const ->
const
end)}
end)
{:ok, child_space} =
create(
Map.values(variable_copies),
propagator_copies,
Keyword.put(data.opts, :parent, data.id)
)
child_space
end)
|> tap(fn new_nodes ->
publish(data, {:nodes, new_nodes})
end)
shutdown(data, :distribute)
end
end
defp branching(variables, search_strategy) do
case search_strategy.select_variable(variables) do
{:ok, var_to_branch_on} ->
var_domain = var_to_branch_on.domain
case search_strategy.partition(var_domain) do
:fail -> :fail
{:ok, partitions} -> {:ok, {var_to_branch_on, partitions}}
end
error ->
error
end
end
defp publish(data, message) do
send(data.solver, message)
end
defp shutdown(%{keep_alive: keep_alive} = data, reason) do
if !keep_alive do
publish(data, {:shutdown_space, {self(), reason}})
{:stop, :normal, data}
else
:keep_state_and_data
end
end
@impl true
def terminate(_reason, _current_state, data) do
cleanup(data)
end
defp cleanup(%{propagator_threads: threads, store: store, variables: variables} = _data) do
Enum.each(threads, fn {_ref, thread} -> PropagatorThread.dispose(thread) end)
ConstraintStore.dispose(store, variables)
end
end