Skip to content

Commit 42f5386

Browse files
committed
WIP: properly handle cons_{get,set}_expr and slack
1 parent 0ebf32a commit 42f5386

File tree

2 files changed

+80
-22
lines changed

2 files changed

+80
-22
lines changed

mip/highs.py

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -638,30 +638,61 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr":
638638
)
639639

640640
# - second, to get the coefficients in pre-allocated arrays.
641-
matrix_start = ffi.new("int[]", 1)
642-
matrix_index = ffi.new("int[]", num_nz[0])
643-
matrix_value = ffi.new("double[]", num_nz[0])
644-
check(
645-
self._lib.Highs_getRowsByRange(
646-
self._model,
647-
row,
648-
row,
649-
num_row,
650-
lower,
651-
upper,
652-
num_nz,
653-
matrix_start,
654-
matrix_index,
655-
matrix_value,
641+
if num_nz[0] == 0:
642+
# early exit for empty expressions
643+
expr = mip.xsum([])
644+
else:
645+
matrix_start = ffi.new("int[]", 1)
646+
matrix_index = ffi.new("int[]", num_nz[0])
647+
matrix_value = ffi.new("double[]", num_nz[0])
648+
check(
649+
self._lib.Highs_getRowsByRange(
650+
self._model,
651+
row,
652+
row,
653+
num_row,
654+
lower,
655+
upper,
656+
num_nz,
657+
matrix_start,
658+
matrix_index,
659+
matrix_value,
660+
)
661+
)
662+
expr = mip.xsum(
663+
matrix_value[i] * self.model.vars[i] for i in range(num_nz[0])
656664
)
657-
)
658665

659-
return mip.xsum(matrix_value[i] * self.model.vars[i] for i in range(num_nz))
666+
# Also set sense and constant
667+
lhs, rhs = lower[0], upper[0]
668+
if rhs < mip.INF:
669+
expr -= rhs
670+
if lhs > -mip.INF:
671+
assert lhs == rhs
672+
expr.sense = mip.EQUAL
673+
else:
674+
expr.sense = mip.LESS_OR_EQUAL
675+
else:
676+
if lhs > -mip.INF:
677+
expr -= lhs
678+
expr.sense = mip.GREATER_OR_EQUAL
679+
else:
680+
raise ValueError("Unbounded constraint?!")
681+
return expr
660682

661683
def constr_set_expr(
662684
self: "SolverHighs", constr: "mip.Constr", value: "mip.LinExpr"
663685
) -> "mip.LinExpr":
664-
raise NotImplementedError()
686+
# We also have to set to 0 all coefficients of the old row, so we
687+
# fetch that first.
688+
coeffs = {var: 0.0 for var in constr.expr}
689+
690+
# Then we fetch the new coefficients and overwrite.
691+
coeffs.update(value.expr.items())
692+
693+
# Finally, we change the coeffs in HiGHS' matrix one-by-one.
694+
for var, coef in coeffs.items():
695+
self._change_coef(constr.idx, var.idx, coef)
665696

666697
def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real:
667698
# fetch both lower and upper bound
@@ -731,7 +762,19 @@ def constr_get_pi(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real:
731762
return self._pi[constr.idx]
732763

733764
def constr_get_slack(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real:
734-
raise NotImplementedError()
765+
expr = constr.expr
766+
activity = sum(coef * var.x for var, coef in expr.expr.items())
767+
rhs = -expr.const
768+
slack = rhs - activity
769+
assert False
770+
if expr.sense == mip.LESS_OR_EQUAL:
771+
return slack
772+
elif expr.sense == mip.GREATER_OR_EQUAL:
773+
return -slack
774+
elif expr.sense == mip.EQUAL:
775+
return -abs(slack)
776+
else:
777+
raise ValueError(f"Invalid constraint sense: {expr.sense}")
735778

736779
def remove_constrs(self: "SolverHighs", constrsList: List[int]):
737780
set_ = ffi.new("int[]", constrsList)

test/test_model.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,18 +1368,29 @@ def test_solve_relaxation(solver):
13681368
y = m.add_var("y", var_type=INTEGER)
13691369
z = m.add_var("z", var_type=BINARY)
13701370

1371-
m.add_constr(x <= 10 * z)
1372-
m.add_constr(x <= 9.5)
1373-
m.add_constr(x + y <= 20)
1371+
c1 = m.add_constr(x <= 10 * z)
1372+
c2 = m.add_constr(x <= 9.5)
1373+
c3 = m.add_constr(x + y <= 20)
13741374
m.objective = mip.maximize(4*x + y - z)
13751375

1376+
# double-check constraint expressions
1377+
assert c1.idx == 0
1378+
expr1 = c1.expr
1379+
assert expr1.expr == pytest.approx({x: 1.0, z: -10.0})
1380+
assert expr1.const == pytest.approx(0.0)
1381+
assert expr1.sense == mip.LESS_OR_EQUAL
1382+
13761383
# first solve proper MIP
13771384
status = m.optimize()
13781385
assert status == OptimizationStatus.OPTIMAL
13791386
assert x.x == pytest.approx(9.5)
13801387
assert y.x == pytest.approx(10.0)
13811388
assert z.x == pytest.approx(1.0)
13821389

1390+
assert c1.slack == pytest.approx(0.5)
1391+
assert c2.slack == pytest.approx(0.0)
1392+
assert c3.slack == pytest.approx(0.0)
1393+
13831394
# then compare LP relaxation
13841395
# (seems to fail for CBC?!)
13851396
if solver == HIGHS:
@@ -1388,3 +1399,7 @@ def test_solve_relaxation(solver):
13881399
assert x.x == pytest.approx(9.5)
13891400
assert y.x == pytest.approx(10.5)
13901401
assert z.x == pytest.approx(0.95)
1402+
1403+
assert c1.slack == pytest.approx(0.0)
1404+
assert c2.slack == pytest.approx(0.0)
1405+
assert c3.slack == pytest.approx(0.0)

0 commit comments

Comments
 (0)