Packages
fixpoint
0.8.48
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/solver.ex
defmodule CPSolver do
@moduledoc """
Solver API.
"""
alias CPSolver.Model
alias CPSolver.Space
alias CPSolver.Constraint
alias CPSolver.Solution
alias CPSolver.Propagator
alias CPSolver.Variable.Interface
alias CPSolver.Shared
use GenServer
require Logger
@default_timeout 30_000
@doc """
"""
@spec solve(Model.t(), Keyword.t()) :: {:ok, map()}
def solve(model, opts \\ []) do
opts = Keyword.merge(Space.default_space_opts(), opts)
shared_data =
Shared.init_shared_data(
space_threads: opts[:space_threads],
distributed: opts[:distributed]
)
|> Map.put(:sync_mode, opts[:sync_mode])
{:ok, solver_pid} =
GenServer.start(CPSolver, [model, Keyword.put(opts, :shared, shared_data)])
{:ok,
shared_data
|> Map.put(:objective, strip_objective(Map.get(model, :objective)))
|> Map.put(:solver_pid, solver_pid)
|> Map.put(
:variable_names,
Enum.map(model.variables, fn var -> Interface.variable(var).name end)
)}
end
defp strip_objective(nil) do
nil
end
defp strip_objective(objective) do
Map.drop(objective, [:variable, :propagator])
end
@spec solve_sync(Model.t(), Keyword.t()) ::
{:ok, map()} | {:error, reason :: any(), info :: any()}
def solve_sync(model, opts \\ []) do
{:ok, solver} = solve(model, Keyword.put(opts, :sync_mode, true))
:ok = wait_for_completion(solver, Keyword.get(opts, :timeout, @default_timeout))
get_results(solver)
|> tap(fn _ -> cleanup(solver) end)
end
defp wait_for_completion(%{complete_flag: complete_flag} = solver, timeout) do
receive do
{:solver_completed, ^complete_flag} -> :ok
after
timeout ->
Logger.error("Timeout waiting on solver completion")
CPSolver.set_complete(solver)
end
end
defp get_results(solver) do
{:ok,
%{
statistics: statistics(solver),
variables: solver.variable_names,
solutions: solutions(solver),
objective: objective_value(solver),
status: status(solver)
}}
end
def stop_spaces(solver) do
Shared.stop_spaces(solver)
end
defp cleanup(solver) do
Shared.cleanup(solver)
end
def statistics(solver) when is_pid(solver) do
GenServer.call(solver, :get_stats)
end
def statistics(solver) when is_map(solver) do
Shared.statistics(solver)
end
def status(solver) do
status(statistics(solver), objective_value(solver), complete?(solver))
end
defp status(%{active_node_count: active_node_count, solution_count: 0}, _objective_value, true)
when active_node_count <= 1 do
:unsatisfiable
end
defp status(%{active_node_count: 0}, objective_value, true) do
(objective_value && {:optimal, objective: objective_value}) || :all_solutions
end
defp status(
%{active_node_count: active_nodes, solution_count: solution_count},
objective_value,
true
)
when active_nodes > 0 do
(objective_value && {:satisfied, objective: objective_value}) ||
(solution_count > 0 && :satisfied) || :unknown
end
defp status(
%{solution_count: solution_count},
objective_value,
false
) do
(objective_value && {:running, solutions_found: solution_count, objective: objective_value}) ||
{:running, solutions_found: solution_count}
end
def solutions(solver) when is_pid(solver) do
GenServer.call(solver, :get_solutions)
end
def solutions(solver) when is_map(solver) do
Shared.solutions(solver)
end
def objective_value(solver) do
Shared.objective_value(solver)
end
def get_state(solver) when is_pid(solver) do
:sys.get_state(solver)
end
def get_state(solver) when is_map(solver) do
get_state(solver.solver_pid)
end
def complete?(solver) when is_map(solver) do
Shared.complete?(solver)
end
def set_complete(solver) do
Shared.set_complete(solver)
end
def dispose(solver) do
cleanup(solver)
Process.exit(solver.solver_pid, :normal)
end
def elapsed_time(solver) do
Shared.elapsed_time(solver)
end
## GenServer callbacks
@impl true
def init([%{constraints: constraints, variables: variables} = model, solver_opts]) do
stop_on = Keyword.get(solver_opts, :stop_on)
## Some data (stats, solutions, possibly more - TBD) has to be shared between spaces
shared = Keyword.get(solver_opts, :shared)
{variables, propagators} = prepare(constraints, variables)
objective = Map.get(model, :objective)
{:ok,
%{
space: nil,
variables: variables,
propagators: propagators,
objective: objective,
shared: Map.put(shared, :objective, objective),
stop_on: stop_on,
solver_opts: solver_opts
}, {:continue, :solve}}
end
@impl true
def handle_continue(
:solve,
%{
variables: variables,
propagators: propagators,
objective: objective,
solver_opts: solver_opts,
shared: shared
} = state
) do
solution_handler_fun =
solver_opts
|> Keyword.get(:solution_handler, Solution.default_handler())
|> build_solution_handler(state)
|> Solution.solution_handler(variables)
{:ok, top_space} =
Space.create(
variables,
propagators,
solver_opts
|> Keyword.put(:objective, objective)
|> Keyword.put(:solver_data, shared)
|> Keyword.delete(:shared)
|> Keyword.put(:solution_handler, solution_handler_fun)
)
{:noreply, Map.put(state, :space, top_space)}
end
@impl true
def handle_info(event, state) do
{:noreply, handle_event(event, state)}
end
def handle_event(_event, state) do
state
end
## Build a solution handler on top of initial one.
## For now, this adds handling logic for stop conditions
defp build_solution_handler(solution_handler, solver_state) do
stop_on_opt = get_in(solver_state, [:solver_opts, :stop_on])
fn solution ->
if not CPSolver.complete?(solver_state.shared) do
solution
|> Solution.run_handler(solution_handler)
|> tap(fn _ -> Shared.add_solution(solver_state.shared, solution) end)
|> tap(fn result -> check_stop_condition(stop_on_opt, result, solution, solver_state) end)
end
end
end
defp check_stop_condition(stop_on_opt, handler_result, solution, solver_state) do
stop_on_opt &&
condition_fun(stop_on_opt).(handler_result, solution, solver_state) &&
Shared.set_complete(solver_state.shared)
end
defp condition_fun({:max_solutions, max_solutions}) do
fn _handler_result, _solution, solver_state ->
solution_count = Shared.statistics(solver_state.shared) |> Map.get(:solution_count, 0)
max_solutions <= solution_count
end
end
defp condition_fun(opts) do
Logger.error("Stop condition with #{inspect(opts)} is not implemented")
end
defp prepare(constraints, variables) do
indexed_variables =
variables
|> Enum.with_index(1)
|> Map.new(fn {v, idx} -> {Interface.id(v), Map.put(Interface.variable(v), :index, idx)} end)
bound_propagators =
Enum.reduce(constraints, [], fn constraint, acc ->
acc ++
Enum.map(Constraint.constraint_to_propagators(constraint), fn p ->
Propagator.bind(p, indexed_variables, :index)
end)
end)
{Map.values(indexed_variables) |> Enum.sort_by(fn v -> Interface.variable(v).index end),
bound_propagators}
end
end