Optex (Optex v0.1.0)

Copy Markdown View Source

Optex: an Elixir library for modeling and solving mixed-integer linear programs, with an in-process HiGHS binding via Rustler.

Build a model with the Optex.DSL, then call optimize/2:

iex> import Optex.DSL
iex> m =
...>   model sense: :max do
...>     variable x, lb: 0.0
...>     variable y, lb: 0.0
...>     constraint x + 2 * y <= 4
...>     constraint 3 * x + y <= 6
...>     objective x + y
...>   end
iex> {:ok, sol} = Optex.optimize(m)
iex> sol.status
:optimal
iex> Float.round(sol.objective, 6)
2.8
iex> {Float.round(sol.values[:x], 6), Float.round(sol.values[:y], 6)}
{1.6, 1.2}

Indexed variables are read back by the same key used to declare them: sol.values[{:y, 2}] for variable y[i], i <- [1, 2, 3].

Summary

Functions

Explain why a model is infeasible.

Transform the model to solver input, solve it, and return the solution with values keyed by the user-facing variable names.

Functions

explain_infeasibility(model, opts \\ [])

@spec explain_infeasibility(
  Optex.Model.t(),
  keyword()
) ::
  {:ok,
   %{
     constraints: list(),
     variables: list(),
     constructs: [{atom(), term()}],
     not_examined: [atom()]
   }}
  | {:error, term()}

Explain why a model is infeasible.

Computes an irreducible infeasible subsystem (IIS): a minimal set of constraints and variable bounds that is infeasible together (for MIPs, of the LP relaxation). Returns {:ok, %{constraints: [...], variables: [...], constructs: [...], not_examined: [...]}} where constraint/variable members are {name_or_id, involvement} (involvement says which side participates: :lower, :upper, :boxed, ...) and construct members are {kind, name_or_id} (defined variables report under their result variable's name). Empty lists mean no IIS was found among what was examined (the model is feasible or the search failed).

Scope depends on the backend. A construct-aware backend (Gurobi, via its native IIS over general and quadratic constraints) examines the FULL model, and conflicting constructs land in constructs. Everywhere else the IIS examines the linear relaxation: constructs (indicators, abs/pwl/min-max definitions, quadratic constraints) are stripped before analysis, so any IIS found is genuine, and not_examined names the stripped construct kinds since the real conflict may live there.

Options: :solver as in optimize/2; the backend must export the optional iis/2 callback of the Optex.Solver behaviour or {:error, :not_supported} is returned.

optimize(model, opts \\ [])

@spec optimize(
  Optex.Model.t(),
  keyword()
) :: {:ok, Optex.Solution.t()} | {:error, term()}

Transform the model to solver input, solve it, and return the solution with values keyed by the user-facing variable names.

Options:

  • :solver - a module implementing the Optex.Solver behaviour. Defaults to Optex.Solver.HiGHS (the only backend in v1; the option is the seam a future backend slots into).

Any remaining options are passed to the solver; every backend understands :time_limit, :mip_gap, :threads, :log, :cancel, and the MIP streaming options :progress (a pid receiving throttled {:optex_progress, map} messages), :progress_every (throttle in ms, default 1000, 0 = unthrottled), and :incumbents (a pid receiving {:optex_incumbent, %{objective: o, values: by_name}} for each improving solution; the values are rekeyed by variable name through a relay process this function manages). Optex.Solver.Gurobi additionally accepts qcp_duals: true to return quadratic constraint duals (in Optex.Solution.qcon_duals, keyed by qconstraint name); backends without that capability reject the option with {:error, {:unsupported, :qcp_duals, backend}}.

Values are keyed by each variable's name: the bare atom for scalar variables (:x), {family, index} for indexed families ({:y, 1}, {:w, {1, :a}}). A variable created without a name falls back to its integer id.