Packages

Fork of tmbb/dantzig tweaked for Advent of Code 2025

Current section

Files

Jump to
dantzig_adventofcode2025 lib dantzig highs.ex
Raw

lib/dantzig/highs.ex

defmodule Dantzig.HiGHS do
@moduledoc false
require Dantzig.Problem, as: Problem
alias Dantzig.Config
alias Dantzig.Constraint
alias Dantzig.ProblemVariable
alias Dantzig.Solution
alias Dantzig.Polynomial
@max_random_prefix 2 ** 32
def solve(%Problem{} = problem) do
iodata = to_lp_iodata(problem)
command = Config.default_highs_binary_path()
with_temporary_files(["model.lp", "solution.lp"], fn [model_path, solution_path] ->
File.write!(model_path, iodata)
{output, _error_code} =
System.cmd(command, [
model_path,
"--solution_file",
solution_path
])
solution_contents =
case File.read(solution_path) do
{:ok, contents} ->
contents
{:error, :enoent} ->
raise RuntimeError, """
Couldn't generate a solution for the given problem.
Input problem/model file:
#{indent(iodata, 4)}
Output from the HiGHS solver:
#{indent(output, 4)}
"""
end
Solution.from_file_contents(solution_contents)
end)
end
defp indent(iodata, indent_level) do
binary = to_string(iodata)
spaces = String.duplicate(" ", indent_level)
binary
|> String.split("\n")
|> Enum.map(fn line -> [spaces, line, "\n"] end)
end
defp with_temporary_files(basenames, fun) do
dir = System.tmp_dir!()
prefix = :rand.uniform(@max_random_prefix) |> Integer.to_string(32)
paths =
for basename <- basenames do
Path.join(dir, "#{prefix}_#{basename}")
end
try do
fun.(paths)
after
for path <- paths do
try do
File.rm!(path)
rescue
_ -> :ok
end
end
end
end
defp constraint_to_iodata(constraint = %Constraint{}) do
[
" ",
constraint.name,
": ",
Polynomial.to_lp_constraint(constraint.left_hand_side),
" ",
operator_to_iodata(constraint.operator),
" ",
to_string(constraint.right_hand_side),
"\n"
]
end
defp operator_to_iodata(operator) do
case operator do
:== -> "="
other -> to_string(other)
end
end
defp direction_to_iodata(:maximize), do: "Maximize"
defp direction_to_iodata(:minimize), do: "Minimize"
def to_lp_iodata(%Problem{} = problem) do
constraints = Enum.sort(problem.constraints)
constraints_iodata =
Enum.map(constraints, fn {_id, constraint} ->
constraint_to_iodata(constraint)
end)
bounds = all_variable_bounds(Map.values(problem.variables))
# need to list the integer variables under General
# https://www.fico.com/fico-xpress-optimization/docs/dms2021-01/solver/optimizer/HTML/chapter10_sec_section102.html
# Variables that do not appear in any of the variable type registration sections (i.e., integers, generals, binaries, semi-continuous, semi integer, partial integer) are defined to be continuous variables by default. That is, there is no section defining variables to be continuous variables.
integer_variables = problem.variables
|> Map.filter(fn {_, v} ->
v.type == :integer
end)
|> Map.keys()
|> Enum.join(" ")
[
direction_to_iodata(problem.direction),
"\n ",
Polynomial.to_lp_iodata_objective(problem.objective),
"\n",
"Subject To\n",
constraints_iodata,
"Bounds\n",
bounds,
"General\n",
integer_variables <> "\n",
"End\n"
]
end
defp variable_bounds(%ProblemVariable{} = v) do
case {v.min, v.max} do
{nil, nil} ->
" #{v.name} free\n"
{nil, max} ->
" #{v.name} <= #{max}\n"
{min, nil} ->
" #{min} <= #{v.name}\n"
{min, max} ->
" #{min} <= #{v.name}\n #{v.name} <= #{max}\n"
end
end
defp all_variable_bounds(variables) do
Enum.map(variables, &variable_bounds/1)
end
end