From d200d746a130078c0e4c49e837dfc06d5f39f154 Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Wed, 25 Mar 2026 10:28:59 +0000 Subject: [PATCH 1/2] Add test demonstrating issue --- tests/test_solvers.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_solvers.py b/tests/test_solvers.py index cac7b4e9a..bff7e9051 100644 --- a/tests/test_solvers.py +++ b/tests/test_solvers.py @@ -1251,3 +1251,22 @@ def test_objective_numexprs(solver, constraint): assert constraint.value() > constraint.get_bounds()[0] # bounds are not always tight, but should be larger than lb for sure except NotSupportedError: pytest.skip(reason=f"{solver} does not support optimisation") + +@pytest.mark.requires_solver("gurobi") +class TestGurobi: + def test_gurobi_read_integers_issue_858(self): + x = cp.intvar(1, 3, shape=2, name="x") + p = cp.intvar(0, 1, shape=3, name="p") + q = cp.intvar(0, 1, shape=3, name="q") + m = cp.Model() + m += x[0] == 1 # TODO without this, x[0] is assigned None because it does not occur in any constraint. This is a separate issue + m += cp.sum([p[0], p[1], p[2]]) == 1 + m += cp.sum([3, 3, 1, -1] * cp.cpm_array([q[0], q[1], q[2], x[1]])) == 0 + m += cp.sum([q[0], q[1], q[2]]) == 1 + + def check(): + print(x, x.value()) + assert (x[1].value() >= 1), f"{x[1]}={x.value()}" + + m.solveAll(solver="gurobi", solution_limit=1000, display=check) + From 2228475a7aaceef2b90c1065453d55733debd26b Mon Sep 17 00:00:00 2001 From: Hendrik 'Henk' Bierlee Date: Wed, 25 Mar 2026 10:29:18 +0000 Subject: [PATCH 2/2] Use `round` i/o `int` when reading int values --- cpmpy/solvers/gurobi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpmpy/solvers/gurobi.py b/cpmpy/solvers/gurobi.py index 26fc3bda4..ada7eebfb 100644 --- a/cpmpy/solvers/gurobi.py +++ b/cpmpy/solvers/gurobi.py @@ -232,12 +232,12 @@ def solve(self, time_limit:Optional[float]=None, solution_callback=None, **kwarg if cpm_var.is_bool(): cpm_var._value = solver_val >= 0.5 else: - cpm_var._value = int(solver_val) + cpm_var._value = round(solver_val) # set _objective_value if self.has_objective(): grb_obj_val = grb_objective.getValue() if round(grb_obj_val) == grb_obj_val: # it is an integer?: - self.objective_value_ = int(grb_obj_val) + self.objective_value_ = round(grb_obj_val) else: # can happen with DirectVar or when using floats as coefficients self.objective_value_ = float(grb_obj_val) @@ -580,7 +580,7 @@ def solveAll(self, display:Optional[Callback]=None, time_limit:Optional[float]=N if cpm_var.is_bool(): cpm_var._value = solver_val >= 0.5 else: - cpm_var._value = int(solver_val) + cpm_var._value = round(solver_val) # Translate objective if self.has_objective():