Current section
Files
Jump to
Current section
Files
lib/dantzig/problem.ex
defmodule Dantzig.Problem do
alias Dantzig.Polynomial
alias Dantzig.ProblemVariable
alias Dantzig.Constraint
alias Dantzig.SolvedConstraint
@nr_of_zeros 8
@type t :: %__MODULE__{}
defstruct variable_counter: 0,
constraint_counter: 0,
objective: Polynomial.const(0.0),
direction: nil,
variables: %{},
constraints: %{},
contraints_metadata: %{}
@spec solve_for_all_variables(t()) :: %{ProblemVariable.variable_namme() => SolvedConstraint.t()}
def solve_for_all_variables(%__MODULE__{} = problem) do
# There are two ways of solving for all variables:
#
# 1. Iterate over all variables and solve constraints that depend on that variable
# 2. Iterate over all constraints and solve for the variables in each constraint
#
# The second one is much more performant, so we pick that one
Enum.reduce(problem.constraints, %{}, fn {_constraint_name, constraint}, solved_constraints ->
# Get all variables from that constraint
variable_names = Polynomial.variables(constraint.left_hand_side)
# Iterate over all constraints
Enum.reduce(variable_names, solved_constraints, fn variable_name, solved_constraints ->
# Put the new solved constraint in the constraint map
solved_constraint = Constraint.solve_for_variable(constraint, variable_name)
Map.update(solved_constraints, variable_name, [solved_constraint], fn constraints ->
[solved_constraint | constraints]
end)
end)
end)
end
@spec new(Keyword.t()) :: t()
def new(opts) when is_list(opts) do
direction =
case Keyword.fetch(opts, :direction) do
{:ok, direction} ->
direction
:error ->
raise RuntimeError, """
Optimization direction is required when creating a Dantzig.Problem. \
Please specify one of :maximize or :minimize\
"""
end
%__MODULE__{direction: direction}
end
@spec add_constraint(t(), Constraint.t(), ConstraintMetadata.t()) :: t()
def add_constraint(problem, constraint, metadata \\ nil) do
constraint_id =
if constraint.name do
"c#{left_pad_with_zeros(problem.constraint_counter)}_#{constraint.name}"
else
"c#{left_pad_with_zeros(problem.constraint_counter)}"
end
# Add the unique ID as a new name for the constraint;
# Because of how we serialize linear problems, the constraint name should be unique.
new_constraint = %{constraint | name: constraint_id, metadata: metadata}
new_constraints = Map.put(problem.constraints, constraint_id, new_constraint)
%{
problem
| constraints: new_constraints,
constraint_counter: problem.constraint_counter + 1
}
end
defp left_pad_with_zeros(number) when is_integer(number) do
String.pad_leading(to_string(number), @nr_of_zeros, "0")
end
def increment_objective(problem, polynomial) do
%{problem | objective: Polynomial.add(problem.objective, polynomial)}
end
def decrement_objective(problem, polynomial) do
%{problem | objective: Polynomial.subtract(problem.objective, polynomial)}
end
def maximize(problem, polynomial) do
case problem.direction do
:minimize -> decrement_objective(problem, polynomial)
:maximize -> increment_objective(problem, polynomial)
end
end
def minimize(problem, polynomial) do
case problem.direction do
:minimize -> increment_objective(problem, polynomial)
:maximize -> decrement_objective(problem, polynomial)
end
end
def new_unmangled_variable(%__MODULE__{} = problem, name, opts \\ []) do
min = Keyword.get(opts, :min, nil)
max = Keyword.get(opts, :max, nil)
type = Keyword.get(opts, :type, :real)
variable = %ProblemVariable{name: name, min: min, max: max, type: type}
monomial = Polynomial.variable(name)
new_variables = Map.put(problem.variables, name, variable)
# Even though we don't use the counter, it's better to increment it
# for the sake of consistency
new_problem = %{
problem
| variables: new_variables,
variable_counter: problem.variable_counter + 1
}
{new_problem, monomial}
end
def new_variable(%__MODULE__{} = problem, suffix, opts \\ []) do
prefix = Keyword.get(opts, :prefix, nil)
name =
case suffix do
bin when is_binary(bin) ->
case prefix do
nil ->
"x#{left_pad_with_zeros(problem.variable_counter)}_#{suffix}"
bin2 when is_binary(bin2) ->
"x#{left_pad_with_zeros(problem.variable_counter)}_#{prefix}_#{suffix}"
end
nil ->
raise "Suffix can't be nil!"
other ->
other
end
min = Keyword.get(opts, :min, nil)
max = Keyword.get(opts, :max, nil)
type = Keyword.get(opts, :type, :real)
# Convert min and max to numbers.
# They might have been converted into polynomials by the
# overloadad operators.
min = min && Polynomial.to_number!(min)
max = max && Polynomial.to_number!(max)
variable = %ProblemVariable{name: name, min: min, max: max, type: type}
monomial = Polynomial.variable(name)
new_variables = Map.put(problem.variables, name, variable)
new_problem = %{
problem
| variables: new_variables,
variable_counter: problem.variable_counter + 1
}
{new_problem, monomial}
end
def new_variables(%__MODULE__{} = problem, prefixes, opts \\ []) do
{new_problem, monomials} =
Enum.reduce(prefixes, {problem, []}, fn new_prefix, {current_problem, current_monomials} ->
{new_problem, new_monomial} = new_variable(current_problem, new_prefix, opts)
{new_problem, [new_monomial | current_monomials]}
end)
{new_problem, Enum.reverse(monomials)}
end
@doc false
def validate_constraint_variables!(problem, left, right) do
syntax_colors = IO.ANSI.syntax_colors()
left_extra =
left
|> Polynomial.variables()
|> Enum.filter(fn var_name -> not Map.has_key?(problem.variables, var_name) end)
right_extra =
right
|> Polynomial.variables()
|> Enum.filter(fn var_name -> not Map.has_key?(problem.variables, var_name) end)
unless left_extra == [] do
raise RuntimeError, """
Error when adding constant to Linear Problem: \
variable(s) #{into_textual_list(left_extra)} \
don't exist in the problem
#{inspect(problem, pretty: true, syntax_colors: syntax_colors)}
#{IO.ANSI.red()}left side of constraint:
#{inspect(left, pretty: true, syntax_colors: syntax_colors)}
"""
end
unless right_extra == [] do
raise RuntimeError, """
Error when adding constant to Linear Problem: \
variable(s) #{into_textual_list(right_extra)} \
don't exist in the problem
#{inspect(problem, pretty: true, syntax_colors: syntax_colors)}
#{IO.ANSI.red()}right side of constraint:
#{inspect(right, pretty: true, syntax_colors: syntax_colors)}
"""
end
end
defp into_textual_list(list) do
Enum.join(list, ", ")
end
end