Packages
fixpoint
0.1.3
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.Propagator.Thread, as: Propagator
alias CPSolver.Solution, as: Solution
alias CPSolver.IntVariable, as: Variable
alias CPSolver.Store.Registry, as: Store
alias CPSolver.Utils
require Logger
@behaviour :gen_statem
defstruct id: nil,
parent: nil,
keep_alive: false,
variables: [],
propagator_threads: %{},
store_impl: Store,
store_handle: nil,
space: nil,
solver: nil,
solution_handler: nil,
search: nil,
opts: []
defp default_space_opts() do
[
store: 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_link(
__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_impl: store_impl, store_handle: store_handle} = _data
) do
Enum.reduce(variables, Map.new(), fn var, acc ->
Map.put(acc, var.id, store_impl.get(store_handle, var, :min))
end)
end
@impl true
def init(args) do
variables = Keyword.get(args, :variables)
propagators = Keyword.get(args, :propagators)
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)
## Subscribe solver to space events
Utils.subscribe(solver, {:space, space_id})
{:ok, space_variables, store} = store_impl.create(variables)
space_data = %Space{
id: space_id,
parent: parent,
keep_alive: keep_alive,
variables: space_variables,
store_impl: store_impl,
store_handle: store,
opts: space_opts,
solution_handler: solution_handler,
search: search_strategy
}
{:ok, :start_propagation, space_data, [{:next_event, :internal, {:propagate, propagators}}]}
end
@impl true
def callback_mode() do
[:state_functions, :state_enter]
end
## Callbacks
def start_propagation(:enter, :start_propagation, data) do
Logger.info("Space #{inspect(self())} started")
{:keep_state, data}
end
def start_propagation(:internal, {:propagate, propagators}, data) do
propagator_threads = start_propagation(propagators)
{: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 = set_propagator_stable(data, propagator_thread, true)
if fixpoint?(updated_data) do
{:next_state, :stable, updated_data}
else
{:keep_state, updated_data}
end
end
def propagating(:info, {:running, propagator_thread}, data) do
Logger.debug("Running propagator #{inspect(propagator_thread)}")
{:keep_state, set_propagator_stable(data, propagator_thread, false)}
end
def propagating(:info, {:entailed, propagator_thread}, data) do
Logger.debug("Entailed propagator #{inspect(propagator_thread)}")
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, {:failed, _propagator_thread}, data) do
{:next_state, :failed, data}
end
def propagating(:info, :solved, data) do
{:next_state, :solved, data}
end
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
@spec stable(any, any, any) :: :keep_state_and_data
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 start_propagation(propagators) do
Enum.reduce(propagators, Map.new(), fn p, acc ->
propagator_id = make_ref()
{:ok, thread} = Propagator.create_thread(self(), p, id: propagator_id)
Map.put(acc, propagator_id, %{thread: thread, propagator: p, stable: false})
end)
end
defp fixpoint?(%{propagator_threads: threads} = _data) do
Enum.all?(threads, fn {_id, thread} -> thread.stable end)
end
defp set_propagator_stable(%{propagator_threads: threads} = data, propagator_id, stable?) do
if Map.has_key?(threads, propagator_id) do
%{
data
| propagator_threads:
Map.update!(threads, propagator_id, fn content ->
Map.put(content, :stable, stable?)
end)
}
else
data
end
end
def update_entailed(%{propagator_threads: threads} = data, propagator_thread) do
Map.put(
data,
:propagator_threads,
Map.delete(threads, propagator_thread)
|> tap(fn m -> Logger.debug("Active propagators: #{inspect(map_size(m))}") end)
)
end
defp solved?(data) do
map_size(data.propagator_threads) == 0
end
defp handle_failure(data) do
Logger.debug("The space #{inspect(data.id)} has failed")
publish(data, :failure)
shutdown(data, :failure)
end
defp handle_solved(%{solution_handler: solution_handler} = data) do
Logger.debug("The space #{inspect(data.id)} has been solved")
data
|> solution()
|> tap(fn solution -> publish(data, {:solution, solution}) end)
|> Solution.run_handler(solution_handler)
shutdown(data, :solved)
end
defp handle_stable(data) do
Logger.debug("Space #{inspect(data.id)} reports stable")
distribute(data)
end
def distribute(
%{
variables: variables
} = data
) do
{variable_clones, all_fixed?} = Utils.localize_variables(variables)
case all_fixed? do
:fail -> handle_failure(data)
true -> handle_solved(data)
false -> do_distribute(data, variable_clones)
end
end
def do_distribute(
%{
propagator_threads: threads,
search: search_strategy
} = data,
variable_clones
) do
Logger.debug("Space #{inspect(data.id)} is distributing...")
case branching(variable_clones, search_strategy) do
:fail ->
handle_failure(data)
{:error, :all_vars_fixed} ->
handle_solved(data)
{: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.new(partition)}
else
{clone_id, Variable.new(clone.domain)}
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
Utils.publish({:space, data.id}, message)
end
defp shutdown(%{keep_alive: keep_alive} = data, reason) do
Logger.info("Space #{inspect(self())} shutdown with #{inspect(reason)}")
if !keep_alive do
publish(data, {:shutdown_space, self()})
## TODO: find a better way to dispose var and propagators
spawn(fn ->
Enum.each(data.propagator_threads, fn {_ref, thread} -> Propagator.dispose(thread) end)
Enum.each(data.variables, fn var -> Variable.dispose(var) end)
end)
{:stop, :normal, data}
else
:keep_state_and_data
end
end
end