Packages
fixpoint
0.21.0
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.Solution
alias CPSolver.Variable.Interface
alias CPSolver.Shared
alias CPSolver.Utils.Vector
use GenServer
require Logger
@default_timeout 30_000
@null_objective InPlace.Array.null()
@doc """
"""
@spec solve_async(Model.t(), Keyword.t()) :: {:ok, map()}
def solve_async(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, shared_data, opts])
{: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(Model.t(), Keyword.t()) ::
{:ok, map()} | {:error, reason :: any(), info :: any()}
def solve(model, opts \\ []) do
{:ok, solver} = solve_async(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
@spec solve_sync(Model.t(), Keyword.t()) ::
{:ok, map()} | {:error, reason :: any(), info :: any()}
@deprecated "Use solve/2 instead"
def solve_sync(model, opts \\ []) do
solve(model, opts)
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
statistics = statistics(solver)
objective = objective_value(solver)
complete? = complete?(solver)
{:ok,
%{
statistics: statistics,
variables: solver.variable_names,
solutions: solutions(solver),
objective: objective,
status: status(statistics, objective, complete?)
}}
end
defp cleanup(solver) do
Shared.cleanup(solver)
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
if solution_count > 0 do
(objective_value && {:satisfied, objective: objective_value}) || :satisfied
else
:unknown
end
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_map(solver) do
Shared.solutions(solver)
end
def objective_value(solver) do
case Shared.objective_value(solver) do
@null_objective -> nil
value -> value
end
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([%{propagators: propagators, variables: variables} = model, shared_data, solver_opts]) do
stop_on = Keyword.get(solver_opts, :stop_on)
## Some data (stats, solutions, possibly more - TBD) has to be shared between spaces
variables = prepare(variables)
objective = Map.get(model, :objective)
{:ok,
%{
space: nil,
variables: variables,
propagators: propagators,
objective: objective,
shared: Map.put(shared_data, :objective, objective),
stop_on: stop_on,
solver_opts: solver_opts
}, {:continue, :solve}}
end
@impl true
def handle_continue(
:solve,
%{
variables: variables,
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(
state,
solver_opts
|> Keyword.put(:solver_data, shared)
|> Keyword.delete(:shared)
|> Keyword.put(:solution_handler, solution_handler_fun)
)
{:noreply, Map.put(state, :space, top_space)}
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(variables) do
## At this point, `variables` list can contain views
## In this case, we will extract variables from views.
Enum.reduce(
Enum.with_index(variables, 1), Vector.new([]), fn {var, idx},
vars_acc ->
Vector.append(vars_acc, Interface.variable(Interface.update(var, :index, idx)))
end)
end
end