From 9f7741d42682b38ce7d915f8e1d717d1bef6812d Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 16 Jan 2025 22:20:12 +0100 Subject: [PATCH 1/8] highs: fix var_get_index for error case --- mip/highs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index b1897b77..f91dbe0c 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -1518,7 +1518,11 @@ def remove_vars(self: "SolverHighs", varsList: List[int]): def var_get_index(self: "SolverHighs", name: str) -> int: idx = ffi.new("int *") - self._lib.Highs_getColByName(self._model, name.encode("utf-8"), idx) + status = self._lib.Highs_getColByName(self._model, name.encode("utf-8"), idx) + if status == STATUS_ERROR: + # This means that no var with that name was found. Unfortunately, + # HiGHS::getColByName doesn't assign a value to idx in that case. + return -1 return idx[0] def get_problem_name(self: "SolverHighs") -> str: From 6c5e313454182ae100963e2753961cf267690040 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 16 Jan 2025 22:20:43 +0100 Subject: [PATCH 2/8] extend tests for var_by_name --- test/mip_test.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/mip_test.py b/test/mip_test.py index 10ab6cb0..053d2b3e 100644 --- a/test/mip_test.py +++ b/test/mip_test.py @@ -556,11 +556,21 @@ def test_constr_by_name_rhs(self, solver): assert model.constr_by_name("row0").rhs == val @pytest.mark.parametrize("solver", SOLVERS) - def test_var_by_name_rhs(self, solver): + def test_var_by_name_valid(self, solver): n, model = self.build_model(solver) - v = model.var_by_name("x({},{})".format(0, 0)) + name = "x({},{})".format(0, 0) + v = model.var_by_name(name) assert v is not None + assert isinstance(v, mip.Var) + assert v.name == name + + @pytest.mark.parametrize("solver", SOLVERS) + def test_var_by_name_invalid(self, solver): + n, model = self.build_model(solver) + + v = model.var_by_name("xyz_invalid_name") + assert v is None @pytest.mark.parametrize("solver", SOLVERS) def test_obj_const1(self, solver: str): From 1649c75fe84a8b27e2015b8d711d0e222e0dff9b Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 17 Jan 2025 16:50:47 +0100 Subject: [PATCH 3/8] extend test_objective: add & rm terms Before, only coefficients were changed. This now fails on HiGHS, which doesn't properly reset the objective before setting a new one. --- test/test_model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_model.py b/test/test_model.py index 00629a55..76c7dd72 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -1356,6 +1356,7 @@ def test_objective(solver): m = Model(solver_name=solver, sense=MAXIMIZE) x = m.add_var(name="x", lb=0, ub=1) y = m.add_var(name="y", lb=0, ub=1) + z = m.add_var(name="z", lb=0, ub=1) m.objective = x - y + 0.5 assert m.objective.x is None @@ -1374,13 +1375,13 @@ def test_objective(solver): # Test changing the objective - m.objective = x + y + 1.5 + m.objective = y + 2*z + 1.5 m.sense = MINIMIZE # TODO: assert m.objective.sense == MINIMIZE assert len(m.objective.expr) == 2 - assert m.objective.expr[x] == 1 assert m.objective.expr[y] == 1 + assert m.objective.expr[z] == 2 assert m.objective.const == 1.5 status = m.optimize() From 3a4bd7bd76bcf97b9a0a74589dd9de6a6c8984d0 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 17 Jan 2025 16:52:45 +0100 Subject: [PATCH 4/8] fix SolverHighs.set_objective --- mip/highs.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mip/highs.py b/mip/highs.py index f91dbe0c..7ca9ee69 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -1035,6 +1035,18 @@ def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): self._lib.Highs_setSolution(self._model, cval, ffi.NULL, ffi.NULL, ffi.NULL) def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = ""): + # first reset old objective (all 0) + n = self.num_cols() + costs = ffi.new("double[]", n) # initialized to 0 + check( + self._lib.Highs_changeColsCostByRange( + self._model, + 0, # from_col + n - 1, # to_col + costs, + ) + ) + # set coefficients for var, coef in lin_expr.expr.items(): check(self._lib.Highs_changeColCost(self._model, var.idx, coef)) From 5de89ae9facb3b7d56d7cc25765532f8da000806 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 17 Jan 2025 16:56:54 +0100 Subject: [PATCH 5/8] highs: add comments about confusion between BINARY and INTEGER --- mip/highs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 7ca9ee69..001d4c78 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -720,7 +720,7 @@ def __init__(self, model: mip.Model, name: str, sense: str): # Buffer string for storing names self._name_buffer = ffi.new(f"char[{self._lib.kHighsMaximumStringLength}]") - # type conversion maps + # type conversion maps (can not distinguish binary from integer!) self._var_type_map = { mip.CONTINUOUS: self._lib.kHighsVarTypeContinuous, mip.BINARY: self._lib.kHighsVarTypeInteger, @@ -815,6 +815,8 @@ def add_var( if name: check(self._lib.Highs_passColName(self._model, col, name.encode("utf-8"))) if var_type != mip.CONTINUOUS: + # Note that HiGHS doesn't distinguish binary and integer variables + # by type. There is only a boolean flag for "integrality". self._num_int_vars += 1 check( self._lib.Highs_changeColIntegrality( From a0d40350c4dd67fb074acf9beb91a12881fa944c Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 17 Jan 2025 16:59:25 +0100 Subject: [PATCH 6/8] add test_verbose --- test/test_model.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_model.py b/test/test_model.py index 76c7dd72..2d6de869 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -1294,6 +1294,21 @@ def test_copy(solver): assert id(term.expr) != id(term_copy.expr) +@skip_on(NotImplementedError) +@pytest.mark.parametrize("solver", SOLVERS) +def test_verbose(solver): + # set and get verbose flag + m = Model(solver_name=solver) + + # active + m.verbose = 1 + assert m.verbose == 1 + + # inactive + m.verbose = 0 + assert m.verbose == 0 + + @skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_constraint_with_lin_expr_and_lin_expr(solver): From db1834dba67fb3ed3ff526b6d822e2b24aa86c7a Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 17 Jan 2025 17:00:56 +0100 Subject: [PATCH 7/8] fix SolverHighs._{get,set}_bool_option_value Used incorrect types before. --- mip/highs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 001d4c78..4e002398 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -760,8 +760,8 @@ def _get_double_option_value(self: "SolverHighs", name: str) -> float: ) return value[0] - def _get_bool_option_value(self: "SolverHighs", name: str) -> float: - value = ffi.new("bool*") + def _get_bool_option_value(self: "SolverHighs", name: str) -> int: + value = ffi.new("int*") check( self._lib.Highs_getBoolOptionValue(self._model, name.encode("UTF-8"), value) ) @@ -779,7 +779,7 @@ def _set_double_option_value(self: "SolverHighs", name: str, value: float): ) ) - def _set_bool_option_value(self: "SolverHighs", name: str, value: float): + def _set_bool_option_value(self: "SolverHighs", name: str, value: int): check( self._lib.Highs_setBoolOptionValue(self._model, name.encode("UTF-8"), value) ) From bcd8d9189f4508231e69395bef010a0f67566be0 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 17 Jan 2025 17:29:25 +0100 Subject: [PATCH 8/8] skip tests with HiGHS based on .gz files --- test/mip_files_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/mip_files_test.py b/test/mip_files_test.py index 785688c9..11a0daf3 100644 --- a/test/mip_files_test.py +++ b/test/mip_files_test.py @@ -139,6 +139,9 @@ def test_mip_file(solver: str, instance: str): max_dif = max(max(abs(ub), abs(lb)) * 0.01, TOL) + if solver == HIGHS and instance.endswith(".gz"): + pytest.skip("HiGHS does not support .gz files.") + m.read(instance) if bas_file: m.verbose = True