Skip to content

Commit 7403f8f

Browse files
committed
highs: support optimize(relax=True)
1 parent c83d8ca commit 7403f8f

File tree

2 files changed

+55
-4
lines changed

2 files changed

+55
-4
lines changed

mip/highs.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,7 @@ def get_objective_const(self: "SolverHighs") -> numbers.Real:
382382
check(self._lib.Highs_getObjectiveOffset(self._model, offset))
383383
return offset[0]
384384

385-
def relax(self: "SolverHighs"):
386-
# change integrality of all columns
385+
def _all_cols_continuous(self: "SolverHighs"):
387386
n = self.num_cols()
388387
integrality = ffi.new(
389388
"int[]", [self._lib.kHighsVarTypeContinuous for i in range(n)]
@@ -393,6 +392,24 @@ def relax(self: "SolverHighs"):
393392
self._model, 0, n - 1, integrality
394393
)
395394
)
395+
396+
def _reset_var_types(self: "SolverHighs"):
397+
var_type_map = {
398+
mip.CONTINUOUS: self._lib.kHighsVarTypeContinuous,
399+
mip.BINARY: self._lib.kHighsVarTypeInteger,
400+
mip.INTEGER: self._lib.kHighsVarTypeInteger,
401+
}
402+
integrality = ffi.new("int[]", [var_type_map[vt] for vt in self._var_type])
403+
n = self.num_cols()
404+
check(
405+
self._lib.Highs_changeColsIntegralityByRange(
406+
self._model, 0, n - 1, integrality
407+
)
408+
)
409+
410+
def relax(self: "SolverHighs"):
411+
# change integrality of all columns
412+
self._all_cols_continuous()
396413
self._var_type = [mip.CONTINUOUS] * len(self._var_type)
397414

398415
def generate_cuts(
@@ -413,8 +430,10 @@ def optimize(
413430
relax: bool = False,
414431
) -> "mip.OptimizationStatus":
415432
if relax:
416-
# TODO: handle relax (need to remember and reset integrality?!
417-
raise NotImplementedError()
433+
# Temporarily change variable types. Original types are still stored
434+
# in self._var_type.
435+
self._all_cols_continuous()
436+
418437
check(self._lib.Highs_run(self._model))
419438

420439
# store solution values for later access
@@ -440,6 +459,10 @@ def optimize(
440459
if self._has_dual_solution():
441460
self._pi = [row_dual[i] for i in range(m)]
442461

462+
if relax:
463+
# Undo the temporary changes.
464+
self._reset_var_types()
465+
443466
return opt_status
444467

445468
def get_objective_value(self: "SolverHighs") -> numbers.Real:

test/test_model.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,3 +1360,31 @@ def test_change_objective_sense(solver):
13601360
status = m.optimize()
13611361
assert status == OptimizationStatus.OPTIMAL
13621362
assert x.x == pytest.approx(10.0)
1363+
1364+
@pytest.mark.parametrize("solver", SOLVERS)
1365+
def test_solve_relaxation(solver):
1366+
m = Model(solver_name=solver)
1367+
x = m.add_var("x", var_type=CONTINUOUS)
1368+
y = m.add_var("y", var_type=INTEGER)
1369+
z = m.add_var("z", var_type=BINARY)
1370+
1371+
m.add_constr(x <= 10 * z)
1372+
m.add_constr(x <= 9.5)
1373+
m.add_constr(x + y <= 20)
1374+
m.objective = mip.maximize(4*x + y - z)
1375+
1376+
# first solve proper MIP
1377+
status = m.optimize()
1378+
assert status == OptimizationStatus.OPTIMAL
1379+
assert x.x == pytest.approx(9.5)
1380+
assert y.x == pytest.approx(10.0)
1381+
assert z.x == pytest.approx(1.0)
1382+
1383+
# then compare LP relaxation
1384+
# (seems to fail for CBC?!)
1385+
if solver == HIGHS:
1386+
status = m.optimize(relax=True)
1387+
assert status == OptimizationStatus.OPTIMAL
1388+
assert x.x == pytest.approx(9.5)
1389+
assert y.x == pytest.approx(10.5)
1390+
assert z.x == pytest.approx(0.95)

0 commit comments

Comments
 (0)