defmodule CPSolver do @moduledoc """ Solver API. """ alias CPSolver.Space use GenServer require Logger @doc """ """ @spec solve(Model.t(), Keyword.t()) :: any() def solve(model, opts \\ []) do {:ok, _solver} = GenServer.start_link(CPSolver, [model, opts]) end def statistics(solver) when is_pid(solver) do GenServer.call(solver, :get_stats) end def solutions(solver) when is_pid(solver) do GenServer.call(solver, :get_solutions) end ## GenServer callbacks @impl true def init([%{constraints: constraints, variables: variables} = _model, solver_opts]) do propagators = Enum.reduce(constraints, [], fn constraint, acc -> acc ++ constraint_to_propagators(constraint) end) stop_on = Keyword.get(solver_opts, :stop_on) {:ok, %{ space: nil, variables: variables, propagators: propagators, solution_count: 0, failure_count: 0, node_count: 1, solutions: [], active_nodes: MapSet.new(), stop_on: stop_on, solver_opts: solver_opts }, {:continue, :solve}} end defp constraint_to_propagators(constraint) do [constraint_mod | args] = Tuple.to_list(constraint) constraint_mod.propagators(args) end @impl true def handle_continue( :solve, %{variables: variables, propagators: propagators, solver_opts: solver_opts} = state ) do {:ok, top_space} = Space.create(variables, propagators, Keyword.put(solver_opts, :solver, self())) {:noreply, Map.put(state, :space, top_space)} end @impl true def handle_info(event, state) do {:noreply, handle_event(event, state)} end defp handle_event( {:solution, new_solution}, %{solution_count: count, solutions: solutions, stop_on: stop_on} = state ) do if check_for_stop(stop_on, new_solution, state) do stop_spaces(state) else %{state | solution_count: count + 1, solutions: [new_solution | solutions]} end ## TODO: check for stopping condition here. ## Q: spaces are async and handle solutions on their own, ## so even if stopping condition is handled here, how do (or should) ## we prevent spaces from emitting new solutions? end defp handle_event(:failure, %{failure_count: count} = state) do Logger.debug("Solver: space failure") %{state | failure_count: count + 1} end defp handle_event({:nodes, new_nodes}, %{node_count: count, active_nodes: nodes} = state) do new_nodes_set = MapSet.new(new_nodes) n = MapSet.size(new_nodes_set) Logger.debug("Solver: #{n} new node(s)") %{state | node_count: count + n, active_nodes: MapSet.union(nodes, new_nodes_set)} end defp handle_event({:shutdown_space, node}, %{active_nodes: nodes} = state) do %{state | active_nodes: MapSet.delete(nodes, node)} end defp handle_event(unexpected, state) do Logger.error("Solver: unexpected message #{inspect(unexpected)}") state end @impl true def handle_call(:get_stats, _from, state) do {:reply, get_stats(state), state} end def handle_call(:get_solutions, _from, state) do {:reply, get_solutions(state), state} end defp get_stats(state) do Map.take(state, [:solution_count, :failure_count, :node_count]) end defp get_solutions(%{solutions: solutions} = _state) do ## Here we piggy-back on the fact that the variables are ordered by their refs ## in spaces, and the order there matches the order within solver state. ## This may likely change, we will probably use var names instead of refs. solutions |> Enum.map(fn solution -> solution |> Enum.sort_by(fn {ref, _value} -> ref end) |> Enum.map(fn {_ref, value} -> value end) end) end defp check_for_stop(nil, _solution, _data) do false end defp check_for_stop({:max_solutions, max}, _solution, data) do max == data.solution_count end defp check_for_stop(condition, solution, data) when is_function(condition, 2) do condition.(solution, data) end defp stop_spaces(%{active_nodes: spaces} = data) do Enum.each(spaces, fn s -> Process.alive?(s) && Process.exit(s, :kill) end) data end end