Solving
The remaining pieces are the objective function and the solver call.
The objective
m.set_objective(expression) takes an expression with an empty frame. The
model's sense, "min" by default, sets the direction. m.solve()
assembles the matrix, calls HiGHS and returns a Solution.
import numpy as np
from nimopt import Model, Param, Set, Sum
P = Set("P", np.array(["lisbon", "porto"]))
W = Set("W", np.array(["berlin", "paris", "rome"]))
cost = Param.from_dense("cost", (P, W), np.array([[2.0, 4.0, 5.0], [3.0, 1.0, 6.0]]))
supply = Param.from_dense("supply", (P,), np.array([30.0, 25.0]))
demand = Param.from_dense("demand", (W,), np.array([20.0, 15.0, 15.0]))
m = Model("transport")
x = m.var("x", (P, W))
m.constraint("supply", Sum(W, x[P, W]) <= supply[P])
m.constraint("demand", Sum(P, x[P, W]) >= demand[W])
m.set_objective(Sum(P, W, cost[P, W] * x[P, W]))
solution = m.solve()
print(solution.status)
print(solution.objective)
Output
optimal
135.0
The status is optimal and the objective value is 135.
Status
status reports the outcome of the solve. status and feasible are
readable after any solve. objective and primal raise ValueError where
feasible is False. They raise at status unbounded and
unbounded_or_infeasible whatever feasible reports. bound and gap are
None at those two statuses. dual raises ValueError where status is not
optimal.
Raising the demand of Berlin to 40 makes total demand 70 against total supply 55. The model is infeasible.
import numpy as np
from nimopt import Model, Param, Set, Sum
P = Set("P", np.array(["lisbon", "porto"]))
W = Set("W", np.array(["berlin", "paris", "rome"]))
cost = Param.from_dense("cost", (P, W), np.array([[2.0, 4.0, 5.0], [3.0, 1.0, 6.0]]))
supply = Param.from_dense("supply", (P,), np.array([30.0, 25.0]))
demand = Param.from_dense("demand", (W,), np.array([40.0, 15.0, 15.0]))
m = Model("transport")
x = m.var("x", (P, W))
m.constraint("supply", Sum(W, x[P, W]) <= supply[P])
m.constraint("demand", Sum(P, x[P, W]) >= demand[W])
m.set_objective(Sum(P, W, cost[P, W] * x[P, W]))
solution = m.solve()
print(solution.status)
solution.objective
Raises ValueError
infeasible
ValueError: status is 'infeasible' and the solver reports no feasible point; read `status` before reading values
For an infeasible model whose cause is not evident, m.session() keeps the
solver instance open and diagnose() returns the conflicting rows. See
Solvers.
A solve stopped at a limit
An option in options() stops the solver early. A solver stopped at a
limit reports the best point it found, and feasible is True for it.
objective and primal then return that point. bound returns what the
solver proved about the optimum, an upper bound under sense max and a
lower bound under sense min. gap returns the relative distance from the
objective to that bound.
A search stopped after one node returns the point the solver found there. The thread count of the solver determines that point. The example below reports the properties every such point has.
import numpy as np
from nimopt import Model, Param, Set, Sum
rng = np.random.default_rng(1)
ITEM = Set("item", np.array([f"i{t}" for t in range(40)]))
BIN = Set("bin", np.array([f"b{t}" for t in range(5)]))
weight = Param.from_dense("weight", (BIN, ITEM), rng.uniform(1, 50, (5, 40)))
value = Param.from_dense("value", (ITEM,), rng.uniform(1, 100, 40))
capacity = Param.from_dense("capacity", (BIN,), np.full(5, 306.0))
m = Model("knapsack", sense="max")
x = m.var("x", (ITEM,), integer=True, upper=1.0)
m.constraint("capacity", Sum(ITEM, weight[BIN, ITEM] * x[ITEM]) <= capacity[BIN])
m.set_objective(Sum(ITEM, value[ITEM] * x[ITEM]))
solution = m.solve(options={"node_limit": 1})
print(f"status: {solution.status}")
print(f"feasible: {solution.feasible}")
print(f"the bound is above the objective: {solution.bound > solution.objective}")
print(f"the gap is positive: {solution.gap > 0.0}")
Output
status: solution_limit
feasible: True
the bound is above the objective: True
the gap is positive: True
The status identifies the limit the solver stopped at. HiGHS reports a stop
at node_limit as solution_limit. gap is None where the solver proved
no bound, and feasible is False where it found no point.
The matrix
m.assemble() builds the coefficient matrix without a solver call and
returns it in CSR form. to_dense() renders it for a model of this size,
and row_of(name) gives the row range of a named constraint.
import numpy as np
from nimopt import Model, Param, Set, Sum
P = Set("P", np.array(["lisbon", "porto"]))
W = Set("W", np.array(["berlin", "paris", "rome"]))
supply = Param.from_dense("supply", (P,), np.array([30.0, 25.0]))
demand = Param.from_dense("demand", (W,), np.array([20.0, 15.0, 15.0]))
m = Model("transport")
x = m.var("x", (P, W))
m.constraint("supply", Sum(W, x[P, W]) <= supply[P])
m.constraint("demand", Sum(P, x[P, W]) >= demand[W])
assembled = m.assemble()
print(m.n_rows, m.n_columns, m.nnz)
print(assembled.row_of("demand"))
print(assembled.to_dense())
Output
5 6 12
slice(2, 5, None)
[[1. 1. 1. 0. 0. 0.]
[0. 0. 0. 1. 1. 1.]
[1. 0. 0. 1. 0. 0.]
[0. 1. 0. 0. 1. 0.]
[0. 0. 1. 0. 0. 1.]]
The six columns are the routes, Lisbon's three followed by Porto's. Rows 0
and 1 are the supply rows, each with a 1 under its plant's three routes.
Rows 2 to 4 are the demand rows, each with a 1 under the two routes into its
warehouse. row_of("demand") returns that range.
Next: Reading the solution.