From d0b6011593641a326ade7dd4e9127daab24455b9 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 5 Jan 2023 17:32:12 +0100 Subject: [PATCH 01/65] add module mip.highs, looking for shared library --- mip/highs.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 mip/highs.py diff --git a/mip/highs.py b/mip/highs.py new file mode 100644 index 00000000..fd7b143b --- /dev/null +++ b/mip/highs.py @@ -0,0 +1,47 @@ +"Python-MIP interface to the HiGHS solver." + +import glob +import logging +import os +import os.path +import sys + +import cffi + +import mip + +logger = logging.getLogger(__name__) + +# try loading the solver library +ffi = cffi.FFI() +try: + # first try user-defined path, if given + ENV_KEY = "PMIP_HIGHS_LIBRARY" + if ENV_KEY in os.environ: + libfile = os.environ[ENV_KEY] + logger.debug("Choosing HiGHS library {libfile} via {ENV_KEY}.") + else: + # try library shipped with highspy packaged + import highspy + + # HACK: find dynamic library in sibling folder + pkg_path = os.path.dirname(highspy.__file__) + libs_path = f"{pkg_path}.libs" + # need library matching operating system + if "linux" in sys.platform.lower(): + pattern = "libhighs-*.so.*" + else: + raise NotImplementedError(f"{sys.platform} not supported!") + # there should only be one match + [libfile] = glob.glob(os.path.join(libs_path, pattern)) + logger.debug("Choosing HiGHS library {libfile} via highspy package.") + + highslib = ffi.dlopen(libfile) + has_highs = True +except Exception as e: + logger.error(f"An error occurred while loading the HiGHS library:\n{e}") + has_highs = False + + +class SolverHighs(mip.Solver): + pass # TODO From 6e000a42ec4f1731bba10067e88dd4373504b160 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 09:39:14 +0100 Subject: [PATCH 02/65] bare minimum pair of methods to test shared library --- mip/highs.py | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index fd7b143b..8813f2a9 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -1,6 +1,7 @@ "Python-MIP interface to the HiGHS solver." import glob +import numbers import logging import os import os.path @@ -42,6 +43,68 @@ logger.error(f"An error occurred while loading the HiGHS library:\n{e}") has_highs = False +HEADER = """ +typedef int HighsInt; + +void* Highs_create(void); +void Highs_destroy(void* highs); +HighsInt Highs_readModel(void* highs, const char* filename); +HighsInt Highs_writeModel(void* highs, const char* filename); +HighsInt Highs_run(void* highs); +HighsInt Highs_getModelStatus(const void* highs); +double Highs_getObjectiveValue(const void* highs); +HighsInt Highs_addVar(void* highs, const double lower, const double upper); +HighsInt Highs_addRow( + void* highs, const double lower, const double upper, const HighsInt num_new_nz, + const HighsInt* index, const double* value +); +HighsInt Highs_changeColIntegrality( + void* highs, const HighsInt col, const HighsInt integrality +); +HighsInt Highs_changeColCost(void* highs, const HighsInt col, const double cost); +HighsInt Highs_changeColBounds( + void* highs, const HighsInt col, const double lower, const double upper +); +HighsInt Highs_getRowsByRange( + const void* highs, const HighsInt from_row, const HighsInt to_row, + HighsInt* num_row, double* lower, double* upper, HighsInt* num_nz, + HighsInt* matrix_start, HighsInt* matrix_index, double* matrix_value +); +""" + +if has_highs: + ffi.cdef(HEADER) + class SolverHighs(mip.Solver): - pass # TODO + def __init__(self, model: mip.Model, name: str, sense: str): + super().__init__(model, name, sense) + + # Store reference to library so that it's not garbage-collected (when we + # just use highslib in __del__, it had already become None)?! + self._lib = highslib + + self._model = highslib.Highs_create() + + def __del__(self): + self._lib.Highs_destroy(self._model) + + def add_var( + self, + name: str = "", + obj: numbers.Real = 0, + lb: numbers.Real = 0, + ub: numbers.Real = mip.INF, + var_type: str = mip.CONTINUOUS, + column: "Column" = None, + ): + pass + + +# create solver for testing +if has_highs: + print("have highs") + model = None + solver = SolverHighs(None, "foo_name", mip.MINIMIZE) +else: + print("don't have highs") From 8a556d937ae27faecc888eb8fa09d7302928a4b6 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 09:43:18 +0100 Subject: [PATCH 03/65] can use wrapper library directly --- mip/highs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 8813f2a9..e8d731a3 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -25,16 +25,16 @@ # try library shipped with highspy packaged import highspy - # HACK: find dynamic library in sibling folder pkg_path = os.path.dirname(highspy.__file__) - libs_path = f"{pkg_path}.libs" + # need library matching operating system if "linux" in sys.platform.lower(): - pattern = "libhighs-*.so.*" + pattern = "highs_bindings.*.so" else: raise NotImplementedError(f"{sys.platform} not supported!") + # there should only be one match - [libfile] = glob.glob(os.path.join(libs_path, pattern)) + [libfile] = glob.glob(os.path.join(pkg_path, pattern)) logger.debug("Choosing HiGHS library {libfile} via highspy package.") highslib = ffi.dlopen(libfile) From f4b73381a1af15fcc080248fd238d689a7dd2cf3 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 09:48:21 +0100 Subject: [PATCH 04/65] add (empty) methods from base class --- mip/highs.py | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 289 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index e8d731a3..3a1292a7 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -6,6 +6,7 @@ import os import os.path import sys +from typing import List, Optional, Tuple, Union import cffi @@ -90,7 +91,7 @@ def __del__(self): self._lib.Highs_destroy(self._model) def add_var( - self, + self: "SolverHighs", name: str = "", obj: numbers.Real = 0, lb: numbers.Real = 0, @@ -100,6 +101,293 @@ def add_var( ): pass + def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): + pass + + def add_lazy_constr(self: "SolverHighs", lin_expr: "mip.LinExpr"): + pass + + def add_sos( + self: "SolverHighs", + sos: List[Tuple["mip.Var", numbers.Real]], + sos_type: int, + ): + pass + + def add_cut(self: "SolverHighs", lin_expr: "mip.LinExpr"): + pass + + def get_objective_bound(self: "SolverHighs") -> numbers.Real: + pass + + def get_objective(self: "SolverHighs") -> "mip.LinExpr": + pass + + def get_objective_const(self: "SolverHighs") -> numbers.Real: + pass + + def relax(self: "SolverHighs"): + pass + + def generate_cuts( + self, + cut_types: Optional[List[mip.CutType]] = None, + depth: int = 0, + npass: int = 0, + max_cuts: int = mip.INT_MAX, + min_viol: numbers.Real = 1e-4, + ) -> "mip.CutPool": + pass + + def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None): + pass + + def optimize( + self: "SolverHighs", + relax: bool = False, + ) -> "mip.OptimizationStatus": + pass + + def get_objective_value(self: "SolverHighs") -> numbers.Real: + pass + + def get_log( + self: "SolverHighs", + ) -> List[Tuple[numbers.Real, Tuple[numbers.Real, numbers.Real]]]: + return [] + + def get_objective_value_i(self: "SolverHighs", i: int) -> numbers.Real: + pass + + def get_num_solutions(self: "SolverHighs") -> int: + pass + + def get_objective_sense(self: "SolverHighs") -> str: + pass + + def set_objective_sense(self: "SolverHighs", sense: str): + pass + + def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): + pass + + def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = ""): + pass + + def set_objective_const(self: "SolverHighs", const: numbers.Real): + pass + + def set_processing_limits( + self: "SolverHighs", + max_time: numbers.Real = mip.INF, + max_nodes: int = mip.INT_MAX, + max_sol: int = mip.INT_MAX, + max_seconds_same_incumbent: float = mip.INF, + max_nodes_same_incumbent: int = mip.INT_MAX, + ): + pass + + def get_max_seconds(self: "SolverHighs") -> numbers.Real: + pass + + def set_max_seconds(self: "SolverHighs", max_seconds: numbers.Real): + pass + + def get_max_solutions(self: "SolverHighs") -> int: + pass + + def set_max_solutions(self: "SolverHighs", max_solutions: int): + pass + + def get_pump_passes(self: "SolverHighs") -> int: + pass + + def set_pump_passes(self: "SolverHighs", passes: int): + pass + + def get_max_nodes(self: "SolverHighs") -> int: + pass + + def set_max_nodes(self: "SolverHighs", max_nodes: int): + pass + + def set_num_threads(self: "SolverHighs", threads: int): + pass + + def write(self: "SolverHighs", file_path: str): + pass + + def read(self: "SolverHighs", file_path: str): + pass + + def num_cols(self: "SolverHighs") -> int: + pass + + def num_rows(self: "SolverHighs") -> int: + pass + + def num_nz(self: "SolverHighs") -> int: + pass + + def num_int(self: "SolverHighs") -> int: + pass + + def get_emphasis(self: "SolverHighs") -> mip.SearchEmphasis: + pass + + def set_emphasis(self: "SolverHighs", emph: mip.SearchEmphasis): + pass + + def get_cutoff(self: "SolverHighs") -> numbers.Real: + pass + + def set_cutoff(self: "SolverHighs", cutoff: numbers.Real): + pass + + def get_mip_gap_abs(self: "SolverHighs") -> numbers.Real: + pass + + def set_mip_gap_abs(self: "SolverHighs", mip_gap_abs: numbers.Real): + pass + + def get_mip_gap(self: "SolverHighs") -> numbers.Real: + pass + + def set_mip_gap(self: "SolverHighs", mip_gap: numbers.Real): + pass + + def get_verbose(self: "SolverHighs") -> int: + pass + + def set_verbose(self: "SolverHighs", verbose: int): + pass + + # Constraint-related getters/setters + + def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": + pass + + def constr_set_expr( + self: "SolverHighs", constr: "mip.Constr", value: "mip.LinExpr" + ) -> "mip.LinExpr": + pass + + def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real: + pass + + def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): + pass + + def constr_get_name(self: "SolverHighs", idx: int) -> str: + pass + + def constr_get_pi(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: + pass + + def constr_get_slack(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: + pass + + def remove_constrs(self: "SolverHighs", constrsList: List[int]): + pass + + def constr_get_index(self: "SolverHighs", name: str) -> int: + pass + + # Variable-related getters/setters + + def var_get_branch_priority(self: "SolverHighs", var: "mip.Var") -> numbers.Real: + pass + + def var_set_branch_priority( + self: "SolverHighs", var: "mip.Var", value: numbers.Real + ): + pass + + def var_get_lb(self: "SolverHighs", var: "mip.Var") -> numbers.Real: + pass + + def var_set_lb(self: "SolverHighs", var: "mip.Var", value: numbers.Real): + pass + + def var_get_ub(self: "SolverHighs", var: "mip.Var") -> numbers.Real: + pass + + def var_set_ub(self: "SolverHighs", var: "mip.Var", value: numbers.Real): + pass + + def var_get_obj(self: "SolverHighs", var: "mip.Var") -> numbers.Real: + pass + + def var_set_obj(self: "SolverHighs", var: "mip.Var", value: numbers.Real): + pass + + def var_get_var_type(self: "SolverHighs", var: "mip.Var") -> str: + pass + + def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str): + pass + + def var_get_column(self: "SolverHighs", var: "mip.Var") -> "Column": + pass + + def var_set_column(self: "SolverHighs", var: "mip.Var", value: "Column"): + pass + + def var_get_rc(self: "SolverHighs", var: "mip.Var") -> numbers.Real: + pass + + def var_get_x(self: "SolverHighs", var: "mip.Var") -> numbers.Real: + """Assumes that the solution is available (should be checked + before calling it""" + + def var_get_xi(self: "SolverHighs", var: "mip.Var", i: int) -> numbers.Real: + pass + + def var_get_name(self: "SolverHighs", idx: int) -> str: + pass + + def remove_vars(self: "SolverHighs", varsList: List[int]): + pass + + def var_get_index(self: "SolverHighs", name: str) -> int: + pass + + def get_problem_name(self: "SolverHighs") -> str: + pass + + def set_problem_name(self: "SolverHighs", name: str): + pass + + def get_status(self: "SolverHighs") -> mip.OptimizationStatus: + pass + + def cgraph_density(self: "SolverHighs") -> float: + """Density of the conflict graph""" + pass + + def conflicting( + self: "SolverHighs", + e1: Union["mip.LinExpr", "mip.Var"], + e2: Union["mip.LinExpr", "mip.Var"], + ) -> bool: + """Checks if two assignment to binary variables are in conflict, + returns none if no conflict graph is available""" + pass + + def conflicting_nodes( + self: "SolverHighs", v1: Union["mip.Var", "mip.LinExpr"] + ) -> Tuple[List["mip.Var"], List["mip.Var"]]: + """Returns all assignment conflicting with the assignment in v1 in the + conflict graph. + """ + pass + + def feature_values(self: "SolverHighs") -> List[float]: + pass + + def feature_names(self: "SolverHighs") -> List[str]: + pass + # create solver for testing if has_highs: From 7dd5edede3474aa430588e7b1437f4567fb11ae2 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 10:24:11 +0100 Subject: [PATCH 05/65] introduce HiGHS to mip package --- mip/constants.py | 1 + mip/model.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/mip/constants.py b/mip/constants.py index cf943dc9..6bd88dca 100644 --- a/mip/constants.py +++ b/mip/constants.py @@ -29,6 +29,7 @@ CPLEX = "CPX" # we plan to support CPLEX in the future GRB = "GRB" GUROBI = "GRB" +HIGHS = "HiGHS" SCIP = "SCIP" # we plan to support SCIP in the future # variable types diff --git a/mip/model.py b/mip/model.py index ed690cc7..1fba51a0 100644 --- a/mip/model.py +++ b/mip/model.py @@ -87,6 +87,10 @@ def __init__( import mip.cbc self.solver = mip.cbc.SolverCbc(self, name, sense) + elif self.solver_name.upper() == "HIGHS": + import mip.highs + + self.solver = mip.highs.SolverHighs(self, name, sense) else: import mip.gurobi @@ -394,6 +398,10 @@ def clear(self: "Model"): import mip.cbc self.solver = mip.cbc.SolverCbc(self, self.name, sense) + elif self.solver_name.upper() == "HIGHS": + import mip.highs + + self.solver = mip.highs.SolverHighs(self, self.name, sense) else: # checking which solvers are available import mip.gurobi From a1bcd53b17335691919c78b47994946bb5b5aadc Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 10:33:11 +0100 Subject: [PATCH 06/65] move (temporary) test code to other file (avoid circular deps) --- highs_test.py | 3 +++ mip/highs.py | 9 --------- 2 files changed, 3 insertions(+), 9 deletions(-) create mode 100644 highs_test.py diff --git a/highs_test.py b/highs_test.py new file mode 100644 index 00000000..cfeea738 --- /dev/null +++ b/highs_test.py @@ -0,0 +1,3 @@ +import mip + +model = mip.Model(solver_name=mip.HIGHS) diff --git a/mip/highs.py b/mip/highs.py index 3a1292a7..a6ed1ebb 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -387,12 +387,3 @@ def feature_values(self: "SolverHighs") -> List[float]: def feature_names(self: "SolverHighs") -> List[str]: pass - - -# create solver for testing -if has_highs: - print("have highs") - model = None - solver = SolverHighs(None, "foo_name", mip.MINIMIZE) -else: - print("don't have highs") From be2459030f66a335189455dc4716f9f863691709 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 10:38:50 +0100 Subject: [PATCH 07/65] SolverHighs.add_var: fix position of name argument --- mip/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index a6ed1ebb..53ede9aa 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -92,12 +92,12 @@ def __del__(self): def add_var( self: "SolverHighs", - name: str = "", obj: numbers.Real = 0, lb: numbers.Real = 0, ub: numbers.Real = mip.INF, var_type: str = mip.CONTINUOUS, column: "Column" = None, + name: str = "", ): pass From 84a437f74e01ff81f835c1525ae937137df9f304 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 10:41:26 +0100 Subject: [PATCH 08/65] implement SolverHighs.add_var --- highs_test.py | 4 ++++ mip/highs.py | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/highs_test.py b/highs_test.py index cfeea738..cf04fd73 100644 --- a/highs_test.py +++ b/highs_test.py @@ -1,3 +1,7 @@ import mip model = mip.Model(solver_name=mip.HIGHS) + +x = model.add_var(name="x") +y = model.add_var(name="y", lb=5, ub=23, var_type=mip.INTEGER) +z = model.add_var(name="z", var_type=mip.BINARY) diff --git a/mip/highs.py b/mip/highs.py index 53ede9aa..689751eb 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -47,6 +47,12 @@ HEADER = """ typedef int HighsInt; +const HighsInt kHighsObjSenseMinimize = 1; +const HighsInt kHighsObjSenseMaximize = -1; + +const HighsInt kHighsVarTypeContinuous = 0; +const HighsInt kHighsVarTypeInteger = 1; + void* Highs_create(void); void Highs_destroy(void* highs); HighsInt Highs_readModel(void* highs, const char* filename); @@ -59,6 +65,7 @@ void* highs, const double lower, const double upper, const HighsInt num_new_nz, const HighsInt* index, const double* value ); +HighsInt Highs_changeObjectiveSense(void* highs, const HighsInt sense); HighsInt Highs_changeColIntegrality( void* highs, const HighsInt col, const HighsInt integrality ); @@ -71,6 +78,7 @@ HighsInt* num_row, double* lower, double* upper, HighsInt* num_nz, HighsInt* matrix_start, HighsInt* matrix_index, double* matrix_value ); +HighsInt Highs_getNumCol(const void* highs); """ if has_highs: @@ -79,14 +87,32 @@ class SolverHighs(mip.Solver): def __init__(self, model: mip.Model, name: str, sense: str): - super().__init__(model, name, sense) + if not has_highs: + raise FileNotFoundError( + "HiGHS not found." + "Please install the `highspy` package, or" + "set the `PMIP_HIGHS_LIBRARY` environment variable." + ) # Store reference to library so that it's not garbage-collected (when we # just use highslib in __del__, it had already become None)?! self._lib = highslib + super().__init__(model, name, sense) + + # Model creation and initialization. self._model = highslib.Highs_create() + sense_map = { + mip.MAXIMIZE: self._lib.kHighsObjSenseMaximize, + mip.MINIMIZE: self._lib.kHighsObjSenseMinimize, + } + status = self._lib.Highs_changeObjectiveSense(self._model, sense_map[sense]) + # TODO: handle status (everywhere) + + # Store additional data here, if HiGHS can't do it. + self.__name = name + def __del__(self): self._lib.Highs_destroy(self._model) @@ -99,7 +125,15 @@ def add_var( column: "Column" = None, name: str = "", ): - pass + # TODO: store variable name (HiGHS doesn't?) + # TODO: handle column data + col: int = self._lib.Highs_getNumCol(self._model) + status = self._lib.Highs_addVar(self._model, lb, ub) + status = self._lib.Highs_changeColCost(self._model, col, obj) + if var_type != mip.CONTINUOUS: + status = self._lib.Highs_changeColIntegrality( + self._model, col, self._lib.kHighsVarTypeInteger + ) def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): pass @@ -353,10 +387,10 @@ def var_get_index(self: "SolverHighs", name: str) -> int: pass def get_problem_name(self: "SolverHighs") -> str: - pass + return self.__name def set_problem_name(self: "SolverHighs", name: str): - pass + self.__name = name def get_status(self: "SolverHighs") -> mip.OptimizationStatus: pass From ad6d1c3527cd33d01b27d6bae2438e746e3893db Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 10:48:07 +0100 Subject: [PATCH 09/65] Mark some methods as not implemented. I think that HiGHS doesn't yet support these features. At least they can't be accessed through the C interface. --- mip/highs.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 689751eb..0bc76baa 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -139,17 +139,17 @@ def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): pass def add_lazy_constr(self: "SolverHighs", lin_expr: "mip.LinExpr"): - pass + raise NotImplementedError() def add_sos( self: "SolverHighs", sos: List[Tuple["mip.Var", numbers.Real]], sos_type: int, ): - pass + raise NotImplementedError() def add_cut(self: "SolverHighs", lin_expr: "mip.LinExpr"): - pass + raise NotImplementedError() def get_objective_bound(self: "SolverHighs") -> numbers.Real: pass @@ -171,10 +171,10 @@ def generate_cuts( max_cuts: int = mip.INT_MAX, min_viol: numbers.Real = 1e-4, ) -> "mip.CutPool": - pass + raise NotImplementedError() def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None): - pass + raise NotImplementedError() def optimize( self: "SolverHighs", @@ -203,7 +203,7 @@ def set_objective_sense(self: "SolverHighs", sense: str): pass def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): - pass + raise NotImplementedError() def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = ""): pass @@ -234,10 +234,10 @@ def set_max_solutions(self: "SolverHighs", max_solutions: int): pass def get_pump_passes(self: "SolverHighs") -> int: - pass + raise NotImplementedError() def set_pump_passes(self: "SolverHighs", passes: int): - pass + raise NotImplementedError() def get_max_nodes(self: "SolverHighs") -> int: pass @@ -330,12 +330,12 @@ def constr_get_index(self: "SolverHighs", name: str) -> int: # Variable-related getters/setters def var_get_branch_priority(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - pass + raise NotImplementedError() def var_set_branch_priority( self: "SolverHighs", var: "mip.Var", value: numbers.Real ): - pass + raise NotImplementedError() def var_get_lb(self: "SolverHighs", var: "mip.Var") -> numbers.Real: pass @@ -397,7 +397,7 @@ def get_status(self: "SolverHighs") -> mip.OptimizationStatus: def cgraph_density(self: "SolverHighs") -> float: """Density of the conflict graph""" - pass + raise NotImplementedError() def conflicting( self: "SolverHighs", @@ -406,7 +406,7 @@ def conflicting( ) -> bool: """Checks if two assignment to binary variables are in conflict, returns none if no conflict graph is available""" - pass + raise NotImplementedError() def conflicting_nodes( self: "SolverHighs", v1: Union["mip.Var", "mip.LinExpr"] @@ -414,10 +414,10 @@ def conflicting_nodes( """Returns all assignment conflicting with the assignment in v1 in the conflict graph. """ - pass + raise NotImplementedError() def feature_values(self: "SolverHighs") -> List[float]: - pass + raise NotImplementedError() def feature_names(self: "SolverHighs") -> List[str]: - pass + raise NotImplementedError() From 28bb359185dedc8b6912d8e1b8728f949b48f561 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 11:03:31 +0100 Subject: [PATCH 10/65] store variable name; don't hide internals with __ prefix --- highs_test.py | 7 +++++++ mip/highs.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/highs_test.py b/highs_test.py index cf04fd73..43b19fe6 100644 --- a/highs_test.py +++ b/highs_test.py @@ -5,3 +5,10 @@ x = model.add_var(name="x") y = model.add_var(name="y", lb=5, ub=23, var_type=mip.INTEGER) z = model.add_var(name="z", var_type=mip.BINARY) + + +# internals +solver = model.solver +print(f"Solver: {solver}") +print(f"Var names: {solver._var_name}") +print(f"Var cols: {solver._var_col}") diff --git a/mip/highs.py b/mip/highs.py index 0bc76baa..47d64ac9 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -111,7 +111,9 @@ def __init__(self, model: mip.Model, name: str, sense: str): # TODO: handle status (everywhere) # Store additional data here, if HiGHS can't do it. - self.__name = name + self._name = name + self._var_name: List[str] = [] + self._var_col: Dict[str, int] = {} def __del__(self): self._lib.Highs_destroy(self._model) @@ -125,7 +127,6 @@ def add_var( column: "Column" = None, name: str = "", ): - # TODO: store variable name (HiGHS doesn't?) # TODO: handle column data col: int = self._lib.Highs_getNumCol(self._model) status = self._lib.Highs_addVar(self._model, lb, ub) @@ -135,6 +136,10 @@ def add_var( self._model, col, self._lib.kHighsVarTypeInteger ) + # store name + self._var_name.append(name) + self._var_col[name] = col + def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): pass @@ -387,10 +392,10 @@ def var_get_index(self: "SolverHighs", name: str) -> int: pass def get_problem_name(self: "SolverHighs") -> str: - return self.__name + return self._name def set_problem_name(self: "SolverHighs", name: str): - self.__name = name + self._name = name def get_status(self: "SolverHighs") -> mip.OptimizationStatus: pass From 1522a7bc3a653ade71d7d37428844af1e724e3a0 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 11:29:15 +0100 Subject: [PATCH 11/65] implement SolverHighs.add_constr --- highs_test.py | 5 +++++ mip/highs.py | 27 ++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/highs_test.py b/highs_test.py index 43b19fe6..5dd94aee 100644 --- a/highs_test.py +++ b/highs_test.py @@ -6,9 +6,14 @@ y = model.add_var(name="y", lb=5, ub=23, var_type=mip.INTEGER) z = model.add_var(name="z", var_type=mip.BINARY) +model += x + y == 99 +model += x <= 99 * z +model += x + y + z >= 1 # internals solver = model.solver print(f"Solver: {solver}") print(f"Var names: {solver._var_name}") print(f"Var cols: {solver._var_col}") +print(f"Cons names: {solver._cons_name}") +print(f"Cons cols: {solver._cons_col}") diff --git a/mip/highs.py b/mip/highs.py index 47d64ac9..a6f9f30a 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -79,6 +79,7 @@ HighsInt* matrix_start, HighsInt* matrix_index, double* matrix_value ); HighsInt Highs_getNumCol(const void* highs); +HighsInt Highs_getNumRow(const void* highs); """ if has_highs: @@ -114,6 +115,8 @@ def __init__(self, model: mip.Model, name: str, sense: str): self._name = name self._var_name: List[str] = [] self._var_col: Dict[str, int] = {} + self._cons_name: List[str] = [] + self._cons_col: Dict[str, int] = {} def __del__(self): self._lib.Highs_destroy(self._model) @@ -141,7 +144,29 @@ def add_var( self._var_col[name] = col def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): - pass + row: int = self._lib.Highs_getNumRow(self._model) + + # equation expressed as two-sided inequality + lower = -lin_expr.const + upper = -lin_expr.const + if lin_expr.sense == mip.LESS_OR_EQUAL: + lower = -mip.INF + elif lin_expr.sense == mip.GREATER_OR_EQUAL: + upper = mip.INF + else: + assert lin_expr.sense == mip.EQUAL + + num_new_nz = len(lin_expr.expr) + index = ffi.new("int[]", [var.idx for var in lin_expr.expr.keys()]) + value = ffi.new("double[]", [coef for coef in lin_expr.expr.values()]) + + status = self._lib.Highs_addRow( + self._model, lower, upper, num_new_nz, index, value + ) + + # store name + self._cons_name.append(name) + self._cons_col[name] = row def add_lazy_constr(self: "SolverHighs", lin_expr: "mip.LinExpr"): raise NotImplementedError() From f35d8085f2b6632fe092d04238dd048374367e73 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Sun, 15 Jan 2023 11:45:55 +0100 Subject: [PATCH 12/65] get_objective_bound --- highs_test.py | 5 ++++- mip/highs.py | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/highs_test.py b/highs_test.py index 5dd94aee..2a243c15 100644 --- a/highs_test.py +++ b/highs_test.py @@ -1,6 +1,7 @@ import mip model = mip.Model(solver_name=mip.HIGHS) +solver = model.solver x = model.add_var(name="x") y = model.add_var(name="y", lb=5, ub=23, var_type=mip.INTEGER) @@ -10,8 +11,10 @@ model += x <= 99 * z model += x + y + z >= 1 +# methods +print(f"objective bound: {model.objective_bound}, {solver.get_objective_bound()}") + # internals -solver = model.solver print(f"Solver: {solver}") print(f"Var names: {solver._var_name}") print(f"Var cols: {solver._var_col}") diff --git a/mip/highs.py b/mip/highs.py index a6f9f30a..29f5edd1 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -80,6 +80,9 @@ ); HighsInt Highs_getNumCol(const void* highs); HighsInt Highs_getNumRow(const void* highs); +HighsInt Highs_getDoubleInfoValue( + const void* highs, const char* info, double* value +); """ if has_highs: @@ -182,7 +185,10 @@ def add_cut(self: "SolverHighs", lin_expr: "mip.LinExpr"): raise NotImplementedError() def get_objective_bound(self: "SolverHighs") -> numbers.Real: - pass + info = "mip_dual_bound".encode("utf-8") + value = ffi.new("double*") + status = self._lib.Highs_getDoubleInfoValue(self._model, info, value) + return value[0] def get_objective(self: "SolverHighs") -> "mip.LinExpr": pass From 8d516da4418e4302ce3d088714895ad0aa102e44 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 26 Jan 2023 16:03:56 +0100 Subject: [PATCH 13/65] some objective-related methods --- highs_test.py | 6 ++++ mip/highs.py | 86 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 21 deletions(-) diff --git a/highs_test.py b/highs_test.py index 2a243c15..636fd076 100644 --- a/highs_test.py +++ b/highs_test.py @@ -11,12 +11,18 @@ model += x <= 99 * z model += x + y + z >= 1 +model.objective = mip.minimize(2*x - 3*y + 23) + # methods +print() print(f"objective bound: {model.objective_bound}, {solver.get_objective_bound()}") +print(f"obj expr: {model.objective}, {solver.get_objective()}") # internals +print() print(f"Solver: {solver}") print(f"Var names: {solver._var_name}") print(f"Var cols: {solver._var_col}") print(f"Cons names: {solver._cons_name}") print(f"Cons cols: {solver._cons_col}") + diff --git a/mip/highs.py b/mip/highs.py index 29f5edd1..aa37bad5 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -65,6 +65,7 @@ void* highs, const double lower, const double upper, const HighsInt num_new_nz, const HighsInt* index, const double* value ); +HighsInt Highs_changeObjectiveOffset(void* highs, const double offset); HighsInt Highs_changeObjectiveSense(void* highs, const HighsInt sense); HighsInt Highs_changeColIntegrality( void* highs, const HighsInt col, const HighsInt integrality @@ -78,6 +79,14 @@ HighsInt* num_row, double* lower, double* upper, HighsInt* num_nz, HighsInt* matrix_start, HighsInt* matrix_index, double* matrix_value ); +HighsInt Highs_getColsByRange( + const void* highs, const HighsInt from_col, const HighsInt to_col, + HighsInt* num_col, double* costs, double* lower, double* upper, + HighsInt* num_nz, HighsInt* matrix_start, HighsInt* matrix_index, + double* matrix_value +); +HighsInt Highs_getObjectiveOffset(const void* highs, double* offset); +HighsInt Highs_getObjectiveSense(const void* highs, HighsInt* sense); HighsInt Highs_getNumCol(const void* highs); HighsInt Highs_getNumRow(const void* highs); HighsInt Highs_getDoubleInfoValue( @@ -106,13 +115,7 @@ def __init__(self, model: mip.Model, name: str, sense: str): # Model creation and initialization. self._model = highslib.Highs_create() - - sense_map = { - mip.MAXIMIZE: self._lib.kHighsObjSenseMaximize, - mip.MINIMIZE: self._lib.kHighsObjSenseMinimize, - } - status = self._lib.Highs_changeObjectiveSense(self._model, sense_map[sense]) - # TODO: handle status (everywhere) + self.set_objective_sense(sense) # Store additional data here, if HiGHS can't do it. self._name = name @@ -134,7 +137,8 @@ def add_var( name: str = "", ): # TODO: handle column data - col: int = self._lib.Highs_getNumCol(self._model) + col: int = self.num_cols() + # TODO: handle status (everywhere) status = self._lib.Highs_addVar(self._model, lb, ub) status = self._lib.Highs_changeColCost(self._model, col, obj) if var_type != mip.CONTINUOUS: @@ -147,7 +151,7 @@ def add_var( self._var_col[name] = col def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): - row: int = self._lib.Highs_getNumRow(self._model) + row: int = self.num_rows() # equation expressed as two-sided inequality lower = -lin_expr.const @@ -191,10 +195,36 @@ def get_objective_bound(self: "SolverHighs") -> numbers.Real: return value[0] def get_objective(self: "SolverHighs") -> "mip.LinExpr": - pass + n = self.num_cols() + num_col = ffi.new("int*") + costs = ffi.new("double[]", n) + lower = ffi.new("double[]", n) + upper = ffi.new("double[]", n) + num_nz = ffi.new("int*") + status = self._lib.Highs_getColsByRange( + self._model, + 0, # from_col + n - 1, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) + obj_expr = mip.xsum( + costs[i] * self.model.vars[i] for i in range(n) if costs[i] != 0.0 + ) + obj_expr.add_const(self.get_objective_const()) + obj_expr.sense = self.get_objective_sense() + return obj_expr def get_objective_const(self: "SolverHighs") -> numbers.Real: - pass + offset = ffi.new("double*") + status = self._lib.Highs_getObjectiveOffset(self._model, offset) + return offset[0] def relax(self: "SolverHighs"): pass @@ -233,19 +263,34 @@ def get_num_solutions(self: "SolverHighs") -> int: pass def get_objective_sense(self: "SolverHighs") -> str: - pass + sense = ffi.new("int*") + status = self._lib.Highs_getObjectiveSense(self._model, sense) + sense_map = { + self._lib.kHighsObjSenseMaximize: mip.MAXIMIZE, + self._lib.kHighsObjSenseMinimize: mip.MINIMIZE, + } + return sense_map[sense[0]] def set_objective_sense(self: "SolverHighs", sense: str): - pass + sense_map = { + mip.MAXIMIZE: self._lib.kHighsObjSenseMaximize, + mip.MINIMIZE: self._lib.kHighsObjSenseMinimize, + } + status = self._lib.Highs_changeObjectiveSense(self._model, sense_map[sense]) def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): raise NotImplementedError() def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = ""): - pass + # set coefficients + for var, coef in lin_expr.expr.items(): + status = self._lib.Highs_changeColCost(self._model, var.idx, coef) + + self.set_objective_const(lin_expr.const) + self.set_objective_sense(lin_expr.sense) def set_objective_const(self: "SolverHighs", const: numbers.Real): - pass + status = self._lib.Highs_changeObjectiveOffset(self._model, const) def set_processing_limits( self: "SolverHighs", @@ -291,10 +336,10 @@ def read(self: "SolverHighs", file_path: str): pass def num_cols(self: "SolverHighs") -> int: - pass + return self._lib.Highs_getNumCol(self._model) def num_rows(self: "SolverHighs") -> int: - pass + return self._lib.Highs_getNumRow(self._model) def num_nz(self: "SolverHighs") -> int: pass @@ -407,20 +452,19 @@ def var_get_rc(self: "SolverHighs", var: "mip.Var") -> numbers.Real: pass def var_get_x(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - """Assumes that the solution is available (should be checked - before calling it""" + pass def var_get_xi(self: "SolverHighs", var: "mip.Var", i: int) -> numbers.Real: pass def var_get_name(self: "SolverHighs", idx: int) -> str: - pass + return self._var_name[idx] def remove_vars(self: "SolverHighs", varsList: List[int]): pass def var_get_index(self: "SolverHighs", name: str) -> int: - pass + return self._var_col[name] def get_problem_name(self: "SolverHighs") -> str: return self._name From 824b82e74968c110cfaaeeda82476854403ba058 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 14:12:34 +0100 Subject: [PATCH 14/65] info values & relax --- highs_test.py | 3 +++ mip/highs.py | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/highs_test.py b/highs_test.py index 636fd076..6db977a6 100644 --- a/highs_test.py +++ b/highs_test.py @@ -26,3 +26,6 @@ print(f"Cons names: {solver._cons_name}") print(f"Cons cols: {solver._cons_col}") +# changes +solver.relax() + diff --git a/mip/highs.py b/mip/highs.py index aa37bad5..f91f17a7 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -70,6 +70,10 @@ HighsInt Highs_changeColIntegrality( void* highs, const HighsInt col, const HighsInt integrality ); +HighsInt Highs_changeColsIntegralityByRange( + void* highs, const HighsInt from_col, const HighsInt to_col, + const HighsInt* integrality +); HighsInt Highs_changeColCost(void* highs, const HighsInt col, const double cost); HighsInt Highs_changeColBounds( void* highs, const HighsInt col, const double lower, const double upper @@ -92,6 +96,9 @@ HighsInt Highs_getDoubleInfoValue( const void* highs, const char* info, double* value ); +HighsInt Highs_getIntInfoValue( + const void* highs, const char* info, int* value +); """ if has_highs: @@ -127,6 +134,20 @@ def __init__(self, model: mip.Model, name: str, sense: str): def __del__(self): self._lib.Highs_destroy(self._model) + def _get_int_info_value(self: "SolverHighs", name: str) -> int: + value = ffi.new("int*") + status = self._lib.Highs_getIntInfoValue( + self._model, name.encode("UTF-8"), value + ) + return value[0] + + def _get_double_info_value(self: "SolverHighs", name: str) -> float: + value = ffi.new("double*") + status = self._lib.Highs_getDoubleInfoValue( + self._model, name.encode("UTF-8"), value + ) + return value[0] + def add_var( self: "SolverHighs", obj: numbers.Real = 0, @@ -189,10 +210,7 @@ def add_cut(self: "SolverHighs", lin_expr: "mip.LinExpr"): raise NotImplementedError() def get_objective_bound(self: "SolverHighs") -> numbers.Real: - info = "mip_dual_bound".encode("utf-8") - value = ffi.new("double*") - status = self._lib.Highs_getDoubleInfoValue(self._model, info, value) - return value[0] + return self._get_double_info_value("mip_dual_bound") def get_objective(self: "SolverHighs") -> "mip.LinExpr": n = self.num_cols() @@ -227,7 +245,14 @@ def get_objective_const(self: "SolverHighs") -> numbers.Real: return offset[0] def relax(self: "SolverHighs"): - pass + # change integrality of all columns + n = self.num_cols() + integrality = ffi.new( + "int[]", [self._lib.kHighsVarTypeContinuous for i in range(n)] + ) + status = self._lib.Highs_changeColsIntegralityByRange( + self._model, 0, n - 1, integrality + ) def generate_cuts( self, From 85f693b1569ea65091ad2d9c6a4b55c2a19f67a9 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 14:13:02 +0100 Subject: [PATCH 15/65] some more not implemented --- mip/highs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index f91f17a7..924c6afb 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -279,10 +279,10 @@ def get_objective_value(self: "SolverHighs") -> numbers.Real: def get_log( self: "SolverHighs", ) -> List[Tuple[numbers.Real, Tuple[numbers.Real, numbers.Real]]]: - return [] + raise NotImplementedError() def get_objective_value_i(self: "SolverHighs", i: int) -> numbers.Real: - pass + raise NotImplementedError() def get_num_solutions(self: "SolverHighs") -> int: pass From b468309e90a1cec18b1fc289070ef46a42613dd5 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 14:35:49 +0100 Subject: [PATCH 16/65] status; optimize --- mip/highs.py | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 924c6afb..0ba9028d 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -53,6 +53,28 @@ const HighsInt kHighsVarTypeContinuous = 0; const HighsInt kHighsVarTypeInteger = 1; +const HighsInt kHighsSolutionStatusNone = 0; +const HighsInt kHighsSolutionStatusInfeasible = 1; +const HighsInt kHighsSolutionStatusFeasible = 2; + +const HighsInt kHighsModelStatusNotset = 0; +const HighsInt kHighsModelStatusLoadError = 1; +const HighsInt kHighsModelStatusModelError = 2; +const HighsInt kHighsModelStatusPresolveError = 3; +const HighsInt kHighsModelStatusSolveError = 4; +const HighsInt kHighsModelStatusPostsolveError = 5; +const HighsInt kHighsModelStatusModelEmpty = 6; +const HighsInt kHighsModelStatusOptimal = 7; +const HighsInt kHighsModelStatusInfeasible = 8; +const HighsInt kHighsModelStatusUnboundedOrInfeasible = 9; +const HighsInt kHighsModelStatusUnbounded = 10; +const HighsInt kHighsModelStatusObjectiveBound = 11; +const HighsInt kHighsModelStatusObjectiveTarget = 12; +const HighsInt kHighsModelStatusTimeLimit = 13; +const HighsInt kHighsModelStatusIterationLimit = 14; +const HighsInt kHighsModelStatusUnknown = 15; +const HighsInt kHighsModelStatusSolutionLimit = 16; + void* Highs_create(void); void Highs_destroy(void* highs); HighsInt Highs_readModel(void* highs, const char* filename); @@ -271,7 +293,11 @@ def optimize( self: "SolverHighs", relax: bool = False, ) -> "mip.OptimizationStatus": - pass + if relax: + # TODO: handle relax (need to remember and reset integrality?! + raise NotImplementedError() + status = self._lib.Highs_run(self._model) + return self.get_status() def get_objective_value(self: "SolverHighs") -> numbers.Real: pass @@ -497,8 +523,45 @@ def get_problem_name(self: "SolverHighs") -> str: def set_problem_name(self: "SolverHighs", name: str): self._name = name + def _get_primal_solution_status(self: "SolverHighs"): + sol_status = ffi.new("int*") + status = self._lib.Highs_getIntInfoValue( + self._model, "primal_solution_status", sol_status + ) + return sol_status[0] + + def _has_primal_solution(self: "SolverHighs"): + return ( + self._get_primal_solution_status() == self._lib.kHighsSolutionStatusFeasible + ) + def get_status(self: "SolverHighs") -> mip.OptimizationStatus: - pass + OS = mip.OptimizationStatus + status_map = { + self._lib.kHighsModelStatusNotset: OS.OTHER, + self._lib.kHighsModelStatusLoadError: OS.ERROR, + self._lib.kHighsModelStatusModelError: OS.ERROR, + self._lib.kHighsModelStatusPresolveError: OS.ERROR, + self._lib.kHighsModelStatusSolveError: OS.ERROR, + self._lib.kHighsModelStatusPostsolveError: OS.ERROR, + self._lib.kHighsModelStatusModelEmpty: OS.OTHER, + self._lib.kHighsModelStatusOptimal: OS.OPTIMAL, + self._lib.kHighsModelStatusInfeasible: OS.INFEASIBLE, + self._lib.kHighsModelStatusUnboundedOrInfeasible: OS.INFEASIBLE, + self._lib.kHighsModelStatusUnbounded: OS.UNBOUNDED, + self._lib.kHighsModelStatusObjectiveBound: None, + self._lib.kHighsModelStatusObjectiveTarget: None, + self._lib.kHighsModelStatusTimeLimit: None, + self._lib.kHighsModelStatusIterationLimit: None, + self._lib.kHighsModelStatusUnknown: OS.OTHER, + self._lib.kHighsModelStatusSolutionLimit: None, + } + highs_status = self._lib.Highs_getModelStatus(self._model) + status = status_map[highs_status] + if status is None: + # depends on solution status + status = OS.FEASIBLE if self._has_primal_solution() else OS.NO_SOLUTION_FOUND + return status def cgraph_density(self: "SolverHighs") -> float: """Density of the conflict graph""" From 8db206b4f456e8f786b8fd71967040522a3df836 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 14:57:30 +0100 Subject: [PATCH 17/65] more getters, add _num_int, more not impl. --- highs_test.py | 1 + mip/highs.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/highs_test.py b/highs_test.py index 6db977a6..c171f497 100644 --- a/highs_test.py +++ b/highs_test.py @@ -21,6 +21,7 @@ # internals print() print(f"Solver: {solver}") +print(f"cols: {solver.num_cols()}, rows: {solver.num_rows()}, nz: {solver.num_nz()}, int: {solver.num_int()},") print(f"Var names: {solver._var_name}") print(f"Var cols: {solver._var_col}") print(f"Cons names: {solver._cons_name}") diff --git a/mip/highs.py b/mip/highs.py index 0ba9028d..9db5289c 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -115,6 +115,7 @@ HighsInt Highs_getObjectiveSense(const void* highs, HighsInt* sense); HighsInt Highs_getNumCol(const void* highs); HighsInt Highs_getNumRow(const void* highs); +HighsInt Highs_getNumNz(const void* highs); HighsInt Highs_getDoubleInfoValue( const void* highs, const char* info, double* value ); @@ -147,11 +148,12 @@ def __init__(self, model: mip.Model, name: str, sense: str): self.set_objective_sense(sense) # Store additional data here, if HiGHS can't do it. - self._name = name + self._name: str = name self._var_name: List[str] = [] self._var_col: Dict[str, int] = {} self._cons_name: List[str] = [] self._cons_col: Dict[str, int] = {} + self._num_int: int = 0 def __del__(self): self._lib.Highs_destroy(self._model) @@ -188,6 +190,7 @@ def add_var( status = self._lib.Highs_changeColIntegrality( self._model, col, self._lib.kHighsVarTypeInteger ) + self._num_int += 1 # store name self._var_name.append(name) @@ -275,6 +278,7 @@ def relax(self: "SolverHighs"): status = self._lib.Highs_changeColsIntegralityByRange( self._model, 0, n - 1, integrality ) + self._num_int = 0 def generate_cuts( self, @@ -393,16 +397,17 @@ def num_rows(self: "SolverHighs") -> int: return self._lib.Highs_getNumRow(self._model) def num_nz(self: "SolverHighs") -> int: - pass + return self._lib.Highs_getNumNz(self._model) def num_int(self: "SolverHighs") -> int: - pass + # Can't be queried easily from C API, so we do our own book keeping :-/ + return self._num_int def get_emphasis(self: "SolverHighs") -> mip.SearchEmphasis: - pass + raise NotImplementedError() def set_emphasis(self: "SolverHighs", emph: mip.SearchEmphasis): - pass + raise NotImplementedError() def get_cutoff(self: "SolverHighs") -> numbers.Real: pass From 07b3966801cd23f72247fc1636771a42048eaeec Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 15:16:05 +0100 Subject: [PATCH 18/65] objective_value (now support optimize) --- highs_test.py | 4 ++++ mip/highs.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/highs_test.py b/highs_test.py index c171f497..d5054203 100644 --- a/highs_test.py +++ b/highs_test.py @@ -13,6 +13,10 @@ model.objective = mip.minimize(2*x - 3*y + 23) +status = model.optimize() +print(f"status: {status}") +print(f"objective value: {model.objective_value}") + # methods print() print(f"objective bound: {model.objective_bound}, {solver.get_objective_bound()}") diff --git a/mip/highs.py b/mip/highs.py index 9db5289c..d767f959 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -304,7 +304,7 @@ def optimize( return self.get_status() def get_objective_value(self: "SolverHighs") -> numbers.Real: - pass + return self._lib.Highs_getObjectiveValue(self._model) def get_log( self: "SolverHighs", From db8e80e81ce652b1301e09bb6f7d6a167a81f93f Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 16:02:18 +0100 Subject: [PATCH 19/65] various options --- highs_test.py | 1 + mip/highs.py | 104 +++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 88 insertions(+), 17 deletions(-) diff --git a/highs_test.py b/highs_test.py index d5054203..ba2196c4 100644 --- a/highs_test.py +++ b/highs_test.py @@ -1,6 +1,7 @@ import mip model = mip.Model(solver_name=mip.HIGHS) +model.verbose = 0 solver = model.solver x = model.add_var(name="x") diff --git a/mip/highs.py b/mip/highs.py index d767f959..01594763 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -122,6 +122,24 @@ HighsInt Highs_getIntInfoValue( const void* highs, const char* info, int* value ); +HighsInt Highs_getIntOptionValue( + const void* highs, const char* option, HighsInt* value +); +HighsInt Highs_getDoubleOptionValue( + const void* highs, const char* option, double* value +); +HighsInt Highs_getBoolOptionValue( + const void* highs, const char* option, bool* value +); +HighsInt Highs_setIntOptionValue( + void* highs, const char* option, const HighsInt value +); +HighsInt Highs_setDoubleOptionValue( + void* highs, const char* option, const double value +); +HighsInt Highs_setBoolOptionValue( + void* highs, const char* option, const bool value +); """ if has_highs: @@ -172,6 +190,42 @@ def _get_double_info_value(self: "SolverHighs", name: str) -> float: ) return value[0] + def _get_int_option_value(self: "SolverHighs", name: str) -> int: + value = ffi.new("int*") + status = self._lib.Highs_getIntOptionValue( + self._model, name.encode("UTF-8"), value + ) + return value[0] + + def _get_double_option_value(self: "SolverHighs", name: str) -> float: + value = ffi.new("double*") + status = self._lib.Highs_getDoubleOptionValue( + self._model, name.encode("UTF-8"), value + ) + return value[0] + + def _get_bool_option_value(self: "SolverHighs", name: str) -> float: + value = ffi.new("bool*") + status = self._lib.Highs_getBoolOptionValue( + self._model, name.encode("UTF-8"), value + ) + return value[0] + + def _set_int_option_value(self: "SolverHighs", name: str, value: int): + status = self._lib.Highs_setIntOptionValue( + self._model, name.encode("UTF-8"), value + ) + + def _set_double_option_value(self: "SolverHighs", name: str, value: float): + status = self._lib.Highs_setDoubleOptionValue( + self._model, name.encode("UTF-8"), value + ) + + def _set_bool_option_value(self: "SolverHighs", name: str, value: float): + status = self._lib.Highs_setBoolOptionValue( + self._model, name.encode("UTF-8"), value + ) + def add_var( self: "SolverHighs", obj: numbers.Real = 0, @@ -315,7 +369,8 @@ def get_objective_value_i(self: "SolverHighs", i: int) -> numbers.Real: raise NotImplementedError() def get_num_solutions(self: "SolverHighs") -> int: - pass + # Multiple solutions are not supported (through C API?). + return 1 if self._has_primal_solution() else 0 def get_objective_sense(self: "SolverHighs") -> str: sense = ffi.new("int*") @@ -355,19 +410,28 @@ def set_processing_limits( max_seconds_same_incumbent: float = mip.INF, max_nodes_same_incumbent: int = mip.INT_MAX, ): - pass + if max_time != mip.INF: + self.set_max_seconds(max_time) + if max_nodes != mip.INT_MAX: + self.set_max_nodes(max_nodes) + if max_sol != mip.INT_MAX: + self.set_max_solutions(max_sol) + if max_seconds_same_incumbent != mip.INF: + raise NotImplementedError("Can't set max_seconds_same_incumbent!") + if max_nodes_same_incumbent != mip.INT_MAX: + self.set_max_nodes_same_incumbent(max_nodes_same_incumbent) def get_max_seconds(self: "SolverHighs") -> numbers.Real: - pass + return self._get_double_option_value("time_limit") def set_max_seconds(self: "SolverHighs", max_seconds: numbers.Real): - pass + self._set_double_option_value("time_limit", max_seconds) def get_max_solutions(self: "SolverHighs") -> int: - pass + return self._get_int_option_value("mip_max_improving_sols") def set_max_solutions(self: "SolverHighs", max_solutions: int): - pass + self._get_int_option_value("mip_max_improving_sols", max_solutions) def get_pump_passes(self: "SolverHighs") -> int: raise NotImplementedError() @@ -376,13 +440,19 @@ def set_pump_passes(self: "SolverHighs", passes: int): raise NotImplementedError() def get_max_nodes(self: "SolverHighs") -> int: - pass + return self._get_int_option_value("mip_max_nodes") def set_max_nodes(self: "SolverHighs", max_nodes: int): - pass + self._set_int_option_value("mip_max_nodes", max_nodes) + + def get_max_nodes_same_incumbent(self: "SolverHighs") -> int: + return self._get_int_option_value("mip_max_stall_nodes") + + def set_max_nodes_same_incumbent(self: "SolverHighs", max_nodes_same_incumbent: int): + self._set_int_option_value("mip_max_stall_nodes", max_nodes_same_incumbent) def set_num_threads(self: "SolverHighs", threads: int): - pass + self._set_int_option_value("threads", threads) def write(self: "SolverHighs", file_path: str): pass @@ -410,28 +480,28 @@ def set_emphasis(self: "SolverHighs", emph: mip.SearchEmphasis): raise NotImplementedError() def get_cutoff(self: "SolverHighs") -> numbers.Real: - pass + return self._get_double_option_value("objective_bound") def set_cutoff(self: "SolverHighs", cutoff: numbers.Real): - pass + self._set_double_option_value("objective_bound", cutoff) def get_mip_gap_abs(self: "SolverHighs") -> numbers.Real: - pass + return self._get_double_option_value("mip_abs_gap") def set_mip_gap_abs(self: "SolverHighs", mip_gap_abs: numbers.Real): - pass + self._set_double_option_value("mip_abs_gap", mip_gap_abs) def get_mip_gap(self: "SolverHighs") -> numbers.Real: - pass + return self._get_double_option_value("mip_rel_gap") def set_mip_gap(self: "SolverHighs", mip_gap: numbers.Real): - pass + self._set_double_option_value("mip_rel_gap", mip_gap) def get_verbose(self: "SolverHighs") -> int: - pass + return self._get_bool_option_value("output_flag") def set_verbose(self: "SolverHighs", verbose: int): - pass + self._set_bool_option_value("output_flag", verbose) # Constraint-related getters/setters From 89ad2f28fd8bc67ab897983c6ed5afcbe8015064 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 16:30:14 +0100 Subject: [PATCH 20/65] read/write --- highs_test.py | 1 + mip/highs.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/highs_test.py b/highs_test.py index ba2196c4..b6e26d3c 100644 --- a/highs_test.py +++ b/highs_test.py @@ -22,6 +22,7 @@ print() print(f"objective bound: {model.objective_bound}, {solver.get_objective_bound()}") print(f"obj expr: {model.objective}, {solver.get_objective()}") +model.write("test.lp") # internals print() diff --git a/mip/highs.py b/mip/highs.py index 01594763..8d8d3f9a 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -455,10 +455,10 @@ def set_num_threads(self: "SolverHighs", threads: int): self._set_int_option_value("threads", threads) def write(self: "SolverHighs", file_path: str): - pass + status = self._lib.Highs_writeModel(self._model, file_path.encode("utf-8")) def read(self: "SolverHighs", file_path: str): - pass + status = self._lib.Highs_readModel(self._model, file_path.encode("utf-8")) def num_cols(self: "SolverHighs") -> int: return self._lib.Highs_getNumCol(self._model) From bbff55c73da15feb3a055166323d0c3400a43b54 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 2 Feb 2023 17:31:41 +0100 Subject: [PATCH 21/65] store solution, add var/cons methods --- highs_test.py | 5 ++ mip/highs.py | 220 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 197 insertions(+), 28 deletions(-) diff --git a/highs_test.py b/highs_test.py index b6e26d3c..2b95b507 100644 --- a/highs_test.py +++ b/highs_test.py @@ -32,7 +32,12 @@ print(f"Var cols: {solver._var_col}") print(f"Cons names: {solver._cons_name}") print(f"Cons cols: {solver._cons_col}") +print(f"Sols: {solver._x}, {solver._rc}, {solver._pi}") # changes solver.relax() +# try again +status = model.optimize() +print() +print(f"Sols: {solver._x}, {solver._rc}, {solver._pi}") diff --git a/mip/highs.py b/mip/highs.py index 8d8d3f9a..11b7cc50 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -140,6 +140,16 @@ HighsInt Highs_setBoolOptionValue( void* highs, const char* option, const bool value ); +HighsInt Highs_getSolution( + const void* highs, double* col_value, double* col_dual, + double* row_value, double* row_dual +); +HighsInt Highs_deleteRowsBySet( + void* highs, const HighsInt num_set_entries, const HighsInt* set +); +HighsInt Highs_deleteColsBySet( + void* highs, const HighsInt num_set_entries, const HighsInt* set +); """ if has_highs: @@ -173,6 +183,11 @@ def __init__(self, model: mip.Model, name: str, sense: str): self._cons_col: Dict[str, int] = {} self._num_int: int = 0 + # Also store solution (when available) + self._x = [] + self._rc = [] + self._pi = [] + def __del__(self): self._lib.Highs_destroy(self._model) @@ -355,6 +370,24 @@ def optimize( # TODO: handle relax (need to remember and reset integrality?! raise NotImplementedError() status = self._lib.Highs_run(self._model) + + # store solution values for later access + if self._has_primal_solution(): + # TODO: also handle primal/dual rays? + n, m = self.num_cols(), self.num_rows() + col_value = ffi.new("double[]", n) + col_dual = ffi.new("double[]", n) + row_value = ffi.new("double[]", m) + row_dual = ffi.new("double[]", m) + status = self._lib.Highs_getSolution( + self._model, col_value, col_dual, row_value, row_dual + ) + self._x = [col_value[j] for j in range(n)] + self._rc = [col_dual[j] for j in range(n)] + + if self._has_dual_solution(): + self._pi = [row_dual[i] for i in range(m)] + return self.get_status() def get_objective_value(self: "SolverHighs") -> numbers.Real: @@ -506,33 +539,96 @@ def set_verbose(self: "SolverHighs", verbose: int): # Constraint-related getters/setters def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": - pass + row = constr.idx + # Call method twice: + # - first, to get the sizes for coefficients, + num_row = ffi.new("int*") + lower = ffi.new("double[]", 1) + upper = ffi.new("double[]", 1) + num_nz = ffi.new("int*") + status = self._lib.Highs_getRowsByRange( + self._model, row, row, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL + ) + + # - second, to get the coefficients in pre-allocated arrays. + matrix_start = ffi.new("int[]", 1) + matrix_index = ffi.new("int[]", num_nz[0]) + matrix_value = ffi.new("double[]", num_nz[0]) + status = self._lib.Highs_getRowsByRange( + self._model, + row, + row, + lower, + upper, + num_nz, + matrix_start, + matrix_index, + matrix_value, + ) + assert matrix[0] == 0 + + return mip.xsum(matrix_value[i] * self.model.vars[i] for i in range(num_nz)) def constr_set_expr( self: "SolverHighs", constr: "mip.Constr", value: "mip.LinExpr" ) -> "mip.LinExpr": - pass + raise NotImplementedError() def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real: - pass + # fetch both lower and upper bound + row = constr.idx + num_row = ffi.new("int*") + lower = ffi.new("double[]", 1) + upper = ffi.new("double[]", 1) + num_nz = ffi.new("int*") + status = self._lib.Highs_getRowsByRange( + self._model, row, row, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL + ) + + # case distinction for sense + if lower[0] == -mip.INF: + return -upper[0] + if upper[0] == mip.INF: + return -lower[0] + assert lower[0] == upper[0] + return -lower[0] def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): - pass + # first need to figure out which bound to change (lower or upper) + num_row = ffi.new("int*") + lower = ffi.new("double[]", 1) + upper = ffi.new("double[]", 1) + num_nz = ffi.new("int*") + status = self._lib.Highs_getRowsByRange( + self._model, idx, idx, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL + ) + + # update bounds as needed + lb, ub = lower[0], upper[0] + if lb != -mip.INF: + lb = -rhs + if ub != mip.INF: + ub = -rhs + + # set new bounds + status = self._lib.Highs_changeRowBounds(self._model, idx, lb, ub) def constr_get_name(self: "SolverHighs", idx: int) -> str: - pass + return self._cons_name(idx) def constr_get_pi(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: - pass + if self._pi: + return self._pi[constr.idx] def constr_get_slack(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: - pass + raise NotImplementedError() def remove_constrs(self: "SolverHighs", constrsList: List[int]): - pass + set_ = ffi.new("int[]", constrsList) + status = self._lib.Highs_deleteRowsBySet(self._model, len(constrsList), set_) def constr_get_index(self: "SolverHighs", name: str) -> int: - pass + return self._cons_col(name) # Variable-related getters/setters @@ -545,49 +641,115 @@ def var_set_branch_priority( raise NotImplementedError() def var_get_lb(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - pass + num_col = ffi.new("int*") + costs = ffi.new("double[]", 1) + lower = ffi.new("double[]", 1) + upper = ffi.new("double[]", 1) + num_nz = ffi.new("int*") + status = self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) + return lower[0] def var_set_lb(self: "SolverHighs", var: "mip.Var", value: numbers.Real): - pass + # can only set both bounds, so we just set the old upper bound + old_upper = self.var_get_ub(var) + status = self._lib.Highs_changeColBounds(self._model, var.idx, value, old_upper) def var_get_ub(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - pass + num_col = ffi.new("int*") + costs = ffi.new("double[]", 1) + lower = ffi.new("double[]", 1) + upper = ffi.new("double[]", 1) + num_nz = ffi.new("int*") + status = self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) + return upper[0] def var_set_ub(self: "SolverHighs", var: "mip.Var", value: numbers.Real): - pass + # can only set both bounds, so we just set the old lower bound + old_lower = self.var_get_lb(var) + status = self._lib.Highs_changeColBounds(self._model, var.idx, old_lower, value) def var_get_obj(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - pass + num_col = ffi.new("int*") + costs = ffi.new("double[]", 1) + lower = ffi.new("double[]", 1) + upper = ffi.new("double[]", 1) + num_nz = ffi.new("int*") + status = self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) + return costs[0] def var_set_obj(self: "SolverHighs", var: "mip.Var", value: numbers.Real): - pass + status = self._lib.Highs_changeColCost(self._model, var.idx, value) def var_get_var_type(self: "SolverHighs", var: "mip.Var") -> str: - pass + # TODO: store var type separately? + raise NotImplementedError() def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str): - pass + # TODO: store var type separately? + raise NotImplementedError() def var_get_column(self: "SolverHighs", var: "mip.Var") -> "Column": - pass + # TODO + raise NotImplementedError() def var_set_column(self: "SolverHighs", var: "mip.Var", value: "Column"): - pass + # TODO + raise NotImplementedError() def var_get_rc(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - pass + # TODO: double-check this! + if self._rc: + self._rc[var.idx] def var_get_x(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - pass + if self._x: + return self._x[var.idx] def var_get_xi(self: "SolverHighs", var: "mip.Var", i: int) -> numbers.Real: - pass + raise NotImplementedError() def var_get_name(self: "SolverHighs", idx: int) -> str: return self._var_name[idx] def remove_vars(self: "SolverHighs", varsList: List[int]): - pass + set_ = ffi.new("int[]", varsList) + status = self._lib.Highs_deleteColsBySet(self._model, len(varsList), set_) def var_get_index(self: "SolverHighs", name: str) -> int: return self._var_col[name] @@ -599,17 +761,19 @@ def set_problem_name(self: "SolverHighs", name: str): self._name = name def _get_primal_solution_status(self: "SolverHighs"): - sol_status = ffi.new("int*") - status = self._lib.Highs_getIntInfoValue( - self._model, "primal_solution_status", sol_status - ) - return sol_status[0] + return self._get_int_info_value("primal_solution_status") def _has_primal_solution(self: "SolverHighs"): return ( self._get_primal_solution_status() == self._lib.kHighsSolutionStatusFeasible ) + def _get_dual_solution_status(self: "SolverHighs"): + return self._get_int_info_value("dual_solution_status") + + def _has_dual_solution(self: "SolverHighs"): + return self._get_dual_solution_status() == self._lib.kHighsSolutionStatusFeasible + def get_status(self: "SolverHighs") -> mip.OptimizationStatus: OS = mip.OptimizationStatus status_map = { From 0b386f3cdc6b9bd6e72cc8cefc65f3c694b80cc9 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 14:10:27 +0100 Subject: [PATCH 22/65] rm temporary test script --- highs_test.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 highs_test.py diff --git a/highs_test.py b/highs_test.py deleted file mode 100644 index 2b95b507..00000000 --- a/highs_test.py +++ /dev/null @@ -1,43 +0,0 @@ -import mip - -model = mip.Model(solver_name=mip.HIGHS) -model.verbose = 0 -solver = model.solver - -x = model.add_var(name="x") -y = model.add_var(name="y", lb=5, ub=23, var_type=mip.INTEGER) -z = model.add_var(name="z", var_type=mip.BINARY) - -model += x + y == 99 -model += x <= 99 * z -model += x + y + z >= 1 - -model.objective = mip.minimize(2*x - 3*y + 23) - -status = model.optimize() -print(f"status: {status}") -print(f"objective value: {model.objective_value}") - -# methods -print() -print(f"objective bound: {model.objective_bound}, {solver.get_objective_bound()}") -print(f"obj expr: {model.objective}, {solver.get_objective()}") -model.write("test.lp") - -# internals -print() -print(f"Solver: {solver}") -print(f"cols: {solver.num_cols()}, rows: {solver.num_rows()}, nz: {solver.num_nz()}, int: {solver.num_int()},") -print(f"Var names: {solver._var_name}") -print(f"Var cols: {solver._var_col}") -print(f"Cons names: {solver._cons_name}") -print(f"Cons cols: {solver._cons_col}") -print(f"Sols: {solver._x}, {solver._rc}, {solver._pi}") - -# changes -solver.relax() - -# try again -status = model.optimize() -print() -print(f"Sols: {solver._x}, {solver._rc}, {solver._pi}") From f36001c646cafa79b5d752fbe57d101502cdef8f Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 14:13:12 +0100 Subject: [PATCH 23/65] add HiGHS to test_model.py (some still failing) --- test/test_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_model.py b/test/test_model.py index 8c251e0f..6258e80a 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -7,6 +7,7 @@ CBC, Column, GUROBI, + HIGHS, LinExpr, Model, MAXIMIZE, @@ -19,7 +20,7 @@ ) TOL = 1e-4 -SOLVERS = [CBC] +SOLVERS = [CBC, HIGHS] if "GUROBI_HOME" in os.environ: SOLVERS += [GUROBI] From 5e5c9ed7f3031a19e78632c7b818c3905bbf3bd0 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 14:23:18 +0100 Subject: [PATCH 24/65] fix: only gather solution/obj.val. if OPTIMAL | FEASIBLE This is assert in test test_maximize_single_continuous_or_integer_variable_with_default_bounds, with an unbounded problem. --- mip/highs.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 11b7cc50..41639cbe 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -372,7 +372,11 @@ def optimize( status = self._lib.Highs_run(self._model) # store solution values for later access - if self._has_primal_solution(): + opt_status = self.get_status() + if opt_status in ( + mip.OptimizationStatus.OPTIMAL, + mip.OptimizationStatus.FEASIBLE, + ): # TODO: also handle primal/dual rays? n, m = self.num_cols(), self.num_rows() col_value = ffi.new("double[]", n) @@ -388,10 +392,12 @@ def optimize( if self._has_dual_solution(): self._pi = [row_dual[i] for i in range(m)] - return self.get_status() + return opt_status def get_objective_value(self: "SolverHighs") -> numbers.Real: - return self._lib.Highs_getObjectiveValue(self._model) + # only give value if we have stored a solution + if self._x: + return self._lib.Highs_getObjectiveValue(self._model) def get_log( self: "SolverHighs", From fdeaf063f0d3a5acf96bfa6f5ce6116de8fa13e0 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 14:26:57 +0100 Subject: [PATCH 25/65] fix: translate UnboundedOrInfeasible to UNBOUNDED, not INFEASIBLE This is assert in a test with an unbounded problem (integer variable). --- mip/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 41639cbe..ce20bf20 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -792,7 +792,7 @@ def get_status(self: "SolverHighs") -> mip.OptimizationStatus: self._lib.kHighsModelStatusModelEmpty: OS.OTHER, self._lib.kHighsModelStatusOptimal: OS.OPTIMAL, self._lib.kHighsModelStatusInfeasible: OS.INFEASIBLE, - self._lib.kHighsModelStatusUnboundedOrInfeasible: OS.INFEASIBLE, + self._lib.kHighsModelStatusUnboundedOrInfeasible: OS.UNBOUNDED, # or INFEASIBLE? self._lib.kHighsModelStatusUnbounded: OS.UNBOUNDED, self._lib.kHighsModelStatusObjectiveBound: None, self._lib.kHighsModelStatusObjectiveTarget: None, From e48b827a99b4100919e260ac75281edec7ab71fe Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 14:35:58 +0100 Subject: [PATCH 26/65] store var_type in wrapper, try to keep synchronized --- mip/highs.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index ce20bf20..e6116ee0 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -179,9 +179,9 @@ def __init__(self, model: mip.Model, name: str, sense: str): self._name: str = name self._var_name: List[str] = [] self._var_col: Dict[str, int] = {} + self._var_type: List[str] = [] self._cons_name: List[str] = [] self._cons_col: Dict[str, int] = {} - self._num_int: int = 0 # Also store solution (when available) self._x = [] @@ -259,11 +259,11 @@ def add_var( status = self._lib.Highs_changeColIntegrality( self._model, col, self._lib.kHighsVarTypeInteger ) - self._num_int += 1 - # store name + # store name & type self._var_name.append(name) self._var_col[name] = col + self._var_type.append(var_type) def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): row: int = self.num_rows() @@ -347,7 +347,7 @@ def relax(self: "SolverHighs"): status = self._lib.Highs_changeColsIntegralityByRange( self._model, 0, n - 1, integrality ) - self._num_int = 0 + self._var_type = [mip.CONTINUOUS] * len(self._var_type) def generate_cuts( self, @@ -509,8 +509,7 @@ def num_nz(self: "SolverHighs") -> int: return self._lib.Highs_getNumNz(self._model) def num_int(self: "SolverHighs") -> int: - # Can't be queried easily from C API, so we do our own book keeping :-/ - return self._num_int + return sum(vt != mip.CONTINUOUS for vt in self._var_type) def get_emphasis(self: "SolverHighs") -> mip.SearchEmphasis: raise NotImplementedError() @@ -723,12 +722,14 @@ def var_set_obj(self: "SolverHighs", var: "mip.Var", value: numbers.Real): status = self._lib.Highs_changeColCost(self._model, var.idx, value) def var_get_var_type(self: "SolverHighs", var: "mip.Var") -> str: - # TODO: store var type separately? - raise NotImplementedError() + return self._var_type[var.idx] def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str): - # TODO: store var type separately? - raise NotImplementedError() + if value != mip.CONTINUOUS: + status = self._lib.Highs_changeColIntegrality( + self._model, var.idx, self._lib.kHighsVarTypeInteger + ) + self._var_type[var.idx] = value def var_get_column(self: "SolverHighs", var: "mip.Var") -> "Column": # TODO From 8d7286d571574f780b2a6fde2b0544778228f2f9 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 14:44:11 +0100 Subject: [PATCH 27/65] workaround: pretend to know about branching priority --- mip/highs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index e6116ee0..1c2effdb 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -638,12 +638,15 @@ def constr_get_index(self: "SolverHighs", name: str) -> int: # Variable-related getters/setters def var_get_branch_priority(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - raise NotImplementedError() + # TODO: Is actually not supported by HiGHS, but we mimic the behavior of + # CBC and simply pretend that it's always 0. + return 0 def var_set_branch_priority( self: "SolverHighs", var: "mip.Var", value: numbers.Real ): - raise NotImplementedError() + # TODO: better raise warning/error instead? + pass def var_get_lb(self: "SolverHighs", var: "mip.Var") -> numbers.Real: num_col = ffi.new("int*") From 4512852c1cbf9756f2f3f3ce18482adae9af986e Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 14:56:02 +0100 Subject: [PATCH 28/65] add var_get_column --- mip/highs.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 1c2effdb..a19245b7 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -735,8 +735,48 @@ def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str): self._var_type[var.idx] = value def var_get_column(self: "SolverHighs", var: "mip.Var") -> "Column": - # TODO - raise NotImplementedError() + # Call method twice: + # - first, to get the sizes for coefficients, + num_col = ffi.new("int*") + costs = ffi.new("double[]", 1) + lower = ffi.new("double[]", 1) + upper = ffi.new("double[]", 1) + num_nz = ffi.new("int*") + status = self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) + # - second, to get the coefficients in pre-allocated arrays. + matrix_start = ffi.new("int[]", 1) + matrix_index = ffi.new("int[]", num_nz[0]) + matrix_value = ffi.new("double[]", num_nz[0]) + status = self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + matrix_start, + matrix_index, + matrix_value, + ) + + return mip.Column( + constrs=[self.model.constrs[matrix_index[i]] for i in range(num_nz[0])], + coeffs=[matrix_value[i] for i in range(num_nz[0])], + ) def var_set_column(self: "SolverHighs", var: "mip.Var", value: "Column"): # TODO From dc92b9227d73854926fdb84cb9e7aba373c903f1 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 15:02:12 +0100 Subject: [PATCH 29/65] fix: add missing return --- mip/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index a19245b7..511af017 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -785,7 +785,7 @@ def var_set_column(self: "SolverHighs", var: "mip.Var", value: "Column"): def var_get_rc(self: "SolverHighs", var: "mip.Var") -> numbers.Real: # TODO: double-check this! if self._rc: - self._rc[var.idx] + return self._rc[var.idx] def var_get_x(self: "SolverHighs", var: "mip.Var") -> numbers.Real: if self._x: From 42fbb01c316f6c071ef4e6eafb07a5f8f8f2b0f5 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 15:07:00 +0100 Subject: [PATCH 30/65] fix: only change objective sense if given --- mip/highs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 511af017..05c74e70 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -436,7 +436,8 @@ def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = "") status = self._lib.Highs_changeColCost(self._model, var.idx, coef) self.set_objective_const(lin_expr.const) - self.set_objective_sense(lin_expr.sense) + if lin_expr.sense: + self.set_objective_sense(lin_expr.sense) def set_objective_const(self: "SolverHighs", const: numbers.Real): status = self._lib.Highs_changeObjectiveOffset(self._model, const) From 424d05f380f2dad5ffbed12a7d79ee5592f6e9e1 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 15:14:36 +0100 Subject: [PATCH 31/65] fixing formatting and linter issues --- mip/highs.py | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 05c74e70..1c0aef05 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -6,7 +6,7 @@ import os import os.path import sys -from typing import List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import cffi @@ -247,7 +247,7 @@ def add_var( lb: numbers.Real = 0, ub: numbers.Real = mip.INF, var_type: str = mip.CONTINUOUS, - column: "Column" = None, + column: "mip.Column" = None, name: str = "", ): # TODO: handle column data @@ -553,7 +553,16 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") status = self._lib.Highs_getRowsByRange( - self._model, row, row, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL + self._model, + row, + row, + num_row, + lower, + upper, + num_nz, + ffi.NULL, + ffi.NULL, + ffi.NULL, ) # - second, to get the coefficients in pre-allocated arrays. @@ -564,6 +573,7 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": self._model, row, row, + num_row, lower, upper, num_nz, @@ -571,7 +581,6 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": matrix_index, matrix_value, ) - assert matrix[0] == 0 return mip.xsum(matrix_value[i] * self.model.vars[i] for i in range(num_nz)) @@ -582,13 +591,21 @@ def constr_set_expr( def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real: # fetch both lower and upper bound - row = constr.idx num_row = ffi.new("int*") lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") status = self._lib.Highs_getRowsByRange( - self._model, row, row, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL + self._model, + idx, + idx, + num_row, + lower, + upper, + num_nz, + ffi.NULL, + ffi.NULL, + ffi.NULL, ) # case distinction for sense @@ -606,7 +623,16 @@ def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") status = self._lib.Highs_getRowsByRange( - self._model, idx, idx, lower, upper, num_nz, ffi.NULL, ffi.NULL, ffi.NULL + self._model, + idx, + idx, + num_row, + lower, + upper, + num_nz, + ffi.NULL, + ffi.NULL, + ffi.NULL, ) # update bounds as needed @@ -735,7 +761,7 @@ def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str): ) self._var_type[var.idx] = value - def var_get_column(self: "SolverHighs", var: "mip.Var") -> "Column": + def var_get_column(self: "SolverHighs", var: "mip.Var") -> "mip.Column": # Call method twice: # - first, to get the sizes for coefficients, num_col = ffi.new("int*") @@ -779,7 +805,7 @@ def var_get_column(self: "SolverHighs", var: "mip.Var") -> "Column": coeffs=[matrix_value[i] for i in range(num_nz[0])], ) - def var_set_column(self: "SolverHighs", var: "mip.Var", value: "Column"): + def var_set_column(self: "SolverHighs", var: "mip.Var", value: "mip.Column"): # TODO raise NotImplementedError() @@ -837,7 +863,8 @@ def get_status(self: "SolverHighs") -> mip.OptimizationStatus: self._lib.kHighsModelStatusModelEmpty: OS.OTHER, self._lib.kHighsModelStatusOptimal: OS.OPTIMAL, self._lib.kHighsModelStatusInfeasible: OS.INFEASIBLE, - self._lib.kHighsModelStatusUnboundedOrInfeasible: OS.UNBOUNDED, # or INFEASIBLE? + self._lib.kHighsModelStatusUnboundedOrInfeasible: OS.UNBOUNDED, + # ... or should it be INFEASIBLE? self._lib.kHighsModelStatusUnbounded: OS.UNBOUNDED, self._lib.kHighsModelStatusObjectiveBound: None, self._lib.kHighsModelStatusObjectiveTarget: None, From cf863d4033ee9ef42a67a60cf67315d609fbd1bc Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 15:24:45 +0100 Subject: [PATCH 32/65] add some tests to improve coverage --- test/test_model.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/test_model.py b/test/test_model.py index 6258e80a..e67bd9af 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -3,6 +3,7 @@ import pytest +import mip from mip import ( CBC, Column, @@ -1315,3 +1316,31 @@ def test_remove(solver): m.remove(constr) m.remove(x) + +@pytest.mark.parametrize("solver", SOLVERS) +def test_add_equation(solver): + m = Model(solver_name=solver) + x = m.add_var("x") + constr = m.add_constr(x == 23.5) + + status = m.optimize() + assert status == OptimizationStatus.OPTIMAL + + assert x.x == pytest.approx(23.5) + +@pytest.mark.parametrize("solver", SOLVERS) +def test_change_objective_sense(solver): + m = Model(solver_name=solver) + x = m.add_var("x", lb=10.0, ub=20.0) + + # first maximize + m.objective = mip.maximize(x) + status = m.optimize() + assert status == OptimizationStatus.OPTIMAL + assert x.x == pytest.approx(20.0) + + # then minimize + m.objective = mip.minimize(x) + status = m.optimize() + assert status == OptimizationStatus.OPTIMAL + assert x.x == pytest.approx(10.0) From 0b264f463754cd05de7ea0c7770b5a5026b82f77 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 15:31:26 +0100 Subject: [PATCH 33/65] test_model: add HiGHS solver conditionally --- test/test_model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/test_model.py b/test/test_model.py index e67bd9af..58d41e94 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -4,6 +4,7 @@ import pytest import mip +import mip.highs from mip import ( CBC, Column, @@ -21,9 +22,11 @@ ) TOL = 1e-4 -SOLVERS = [CBC, HIGHS] +SOLVERS = [CBC] if "GUROBI_HOME" in os.environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] # Overall Optimization Tests From 157b817e68f76b29a70fc297b1229dda56982023 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 15:44:28 +0100 Subject: [PATCH 34/65] highs: check return status for each API call --- mip/highs.py | 362 ++++++++++++++++++++++++++++----------------------- 1 file changed, 201 insertions(+), 161 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 1c0aef05..7aa3b5d0 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -47,6 +47,10 @@ HEADER = """ typedef int HighsInt; +const HighsInt kHighsStatusError = -1; +const HighsInt kHighsStatusOk = 0; +const HighsInt kHighsStatusWarning = 1; + const HighsInt kHighsObjSenseMinimize = 1; const HighsInt kHighsObjSenseMaximize = -1; @@ -155,6 +159,14 @@ if has_highs: ffi.cdef(HEADER) +STATUS_ERROR = highslib.kHighsStatusError + + +def check(status): + "Check return status and raise error if not OK." + if status == STATUS_ERROR: + raise mip.InterfacingError("Unknown error in call to HiGHS.") + class SolverHighs(mip.Solver): def __init__(self, model: mip.Model, name: str, sense: str): @@ -193,52 +205,54 @@ def __del__(self): def _get_int_info_value(self: "SolverHighs", name: str) -> int: value = ffi.new("int*") - status = self._lib.Highs_getIntInfoValue( - self._model, name.encode("UTF-8"), value - ) + check(self._lib.Highs_getIntInfoValue(self._model, name.encode("UTF-8"), value)) return value[0] def _get_double_info_value(self: "SolverHighs", name: str) -> float: value = ffi.new("double*") - status = self._lib.Highs_getDoubleInfoValue( - self._model, name.encode("UTF-8"), value + check( + self._lib.Highs_getDoubleInfoValue(self._model, name.encode("UTF-8"), value) ) return value[0] def _get_int_option_value(self: "SolverHighs", name: str) -> int: value = ffi.new("int*") - status = self._lib.Highs_getIntOptionValue( - self._model, name.encode("UTF-8"), value + check( + self._lib.Highs_getIntOptionValue(self._model, name.encode("UTF-8"), value) ) return value[0] def _get_double_option_value(self: "SolverHighs", name: str) -> float: value = ffi.new("double*") - status = self._lib.Highs_getDoubleOptionValue( - self._model, name.encode("UTF-8"), value + check( + self._lib.Highs_getDoubleOptionValue( + self._model, name.encode("UTF-8"), value + ) ) return value[0] def _get_bool_option_value(self: "SolverHighs", name: str) -> float: value = ffi.new("bool*") - status = self._lib.Highs_getBoolOptionValue( - self._model, name.encode("UTF-8"), value + check( + self._lib.Highs_getBoolOptionValue(self._model, name.encode("UTF-8"), value) ) return value[0] def _set_int_option_value(self: "SolverHighs", name: str, value: int): - status = self._lib.Highs_setIntOptionValue( - self._model, name.encode("UTF-8"), value + check( + self._lib.Highs_setIntOptionValue(self._model, name.encode("UTF-8"), value) ) def _set_double_option_value(self: "SolverHighs", name: str, value: float): - status = self._lib.Highs_setDoubleOptionValue( - self._model, name.encode("UTF-8"), value + check( + self._lib.Highs_setDoubleOptionValue( + self._model, name.encode("UTF-8"), value + ) ) def _set_bool_option_value(self: "SolverHighs", name: str, value: float): - status = self._lib.Highs_setBoolOptionValue( - self._model, name.encode("UTF-8"), value + check( + self._lib.Highs_setBoolOptionValue(self._model, name.encode("UTF-8"), value) ) def add_var( @@ -252,12 +266,13 @@ def add_var( ): # TODO: handle column data col: int = self.num_cols() - # TODO: handle status (everywhere) - status = self._lib.Highs_addVar(self._model, lb, ub) - status = self._lib.Highs_changeColCost(self._model, col, obj) + check(self._lib.Highs_addVar(self._model, lb, ub)) + check(self._lib.Highs_changeColCost(self._model, col, obj)) if var_type != mip.CONTINUOUS: - status = self._lib.Highs_changeColIntegrality( - self._model, col, self._lib.kHighsVarTypeInteger + check( + self._lib.Highs_changeColIntegrality( + self._model, col, self._lib.kHighsVarTypeInteger + ) ) # store name & type @@ -282,8 +297,8 @@ def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): index = ffi.new("int[]", [var.idx for var in lin_expr.expr.keys()]) value = ffi.new("double[]", [coef for coef in lin_expr.expr.values()]) - status = self._lib.Highs_addRow( - self._model, lower, upper, num_new_nz, index, value + check( + self._lib.Highs_addRow(self._model, lower, upper, num_new_nz, index, value) ) # store name @@ -313,18 +328,20 @@ def get_objective(self: "SolverHighs") -> "mip.LinExpr": lower = ffi.new("double[]", n) upper = ffi.new("double[]", n) num_nz = ffi.new("int*") - status = self._lib.Highs_getColsByRange( - self._model, - 0, # from_col - n - 1, # to_col - num_col, - costs, - lower, - upper, - num_nz, - ffi.NULL, # matrix_start - ffi.NULL, # matrix_index - ffi.NULL, # matrix_value + check( + self._lib.Highs_getColsByRange( + self._model, + 0, # from_col + n - 1, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) ) obj_expr = mip.xsum( costs[i] * self.model.vars[i] for i in range(n) if costs[i] != 0.0 @@ -335,7 +352,7 @@ def get_objective(self: "SolverHighs") -> "mip.LinExpr": def get_objective_const(self: "SolverHighs") -> numbers.Real: offset = ffi.new("double*") - status = self._lib.Highs_getObjectiveOffset(self._model, offset) + check(self._lib.Highs_getObjectiveOffset(self._model, offset)) return offset[0] def relax(self: "SolverHighs"): @@ -344,8 +361,10 @@ def relax(self: "SolverHighs"): integrality = ffi.new( "int[]", [self._lib.kHighsVarTypeContinuous for i in range(n)] ) - status = self._lib.Highs_changeColsIntegralityByRange( - self._model, 0, n - 1, integrality + check( + self._lib.Highs_changeColsIntegralityByRange( + self._model, 0, n - 1, integrality + ) ) self._var_type = [mip.CONTINUOUS] * len(self._var_type) @@ -369,7 +388,7 @@ def optimize( if relax: # TODO: handle relax (need to remember and reset integrality?! raise NotImplementedError() - status = self._lib.Highs_run(self._model) + check(self._lib.Highs_run(self._model)) # store solution values for later access opt_status = self.get_status() @@ -383,8 +402,10 @@ def optimize( col_dual = ffi.new("double[]", n) row_value = ffi.new("double[]", m) row_dual = ffi.new("double[]", m) - status = self._lib.Highs_getSolution( - self._model, col_value, col_dual, row_value, row_dual + check( + self._lib.Highs_getSolution( + self._model, col_value, col_dual, row_value, row_dual + ) ) self._x = [col_value[j] for j in range(n)] self._rc = [col_dual[j] for j in range(n)] @@ -413,7 +434,7 @@ def get_num_solutions(self: "SolverHighs") -> int: def get_objective_sense(self: "SolverHighs") -> str: sense = ffi.new("int*") - status = self._lib.Highs_getObjectiveSense(self._model, sense) + check(self._lib.Highs_getObjectiveSense(self._model, sense)) sense_map = { self._lib.kHighsObjSenseMaximize: mip.MAXIMIZE, self._lib.kHighsObjSenseMinimize: mip.MINIMIZE, @@ -425,7 +446,7 @@ def set_objective_sense(self: "SolverHighs", sense: str): mip.MAXIMIZE: self._lib.kHighsObjSenseMaximize, mip.MINIMIZE: self._lib.kHighsObjSenseMinimize, } - status = self._lib.Highs_changeObjectiveSense(self._model, sense_map[sense]) + check(self._lib.Highs_changeObjectiveSense(self._model, sense_map[sense])) def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): raise NotImplementedError() @@ -433,14 +454,14 @@ def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = ""): # set coefficients for var, coef in lin_expr.expr.items(): - status = self._lib.Highs_changeColCost(self._model, var.idx, coef) + check(self._lib.Highs_changeColCost(self._model, var.idx, coef)) self.set_objective_const(lin_expr.const) if lin_expr.sense: self.set_objective_sense(lin_expr.sense) def set_objective_const(self: "SolverHighs", const: numbers.Real): - status = self._lib.Highs_changeObjectiveOffset(self._model, const) + check(self._lib.Highs_changeObjectiveOffset(self._model, const)) def set_processing_limits( self: "SolverHighs", @@ -495,10 +516,10 @@ def set_num_threads(self: "SolverHighs", threads: int): self._set_int_option_value("threads", threads) def write(self: "SolverHighs", file_path: str): - status = self._lib.Highs_writeModel(self._model, file_path.encode("utf-8")) + check(self._lib.Highs_writeModel(self._model, file_path.encode("utf-8"))) def read(self: "SolverHighs", file_path: str): - status = self._lib.Highs_readModel(self._model, file_path.encode("utf-8")) + check(self._lib.Highs_readModel(self._model, file_path.encode("utf-8"))) def num_cols(self: "SolverHighs") -> int: return self._lib.Highs_getNumCol(self._model) @@ -552,34 +573,38 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") - status = self._lib.Highs_getRowsByRange( - self._model, - row, - row, - num_row, - lower, - upper, - num_nz, - ffi.NULL, - ffi.NULL, - ffi.NULL, + check( + self._lib.Highs_getRowsByRange( + self._model, + row, + row, + num_row, + lower, + upper, + num_nz, + ffi.NULL, + ffi.NULL, + ffi.NULL, + ) ) # - second, to get the coefficients in pre-allocated arrays. matrix_start = ffi.new("int[]", 1) matrix_index = ffi.new("int[]", num_nz[0]) matrix_value = ffi.new("double[]", num_nz[0]) - status = self._lib.Highs_getRowsByRange( - self._model, - row, - row, - num_row, - lower, - upper, - num_nz, - matrix_start, - matrix_index, - matrix_value, + check( + self._lib.Highs_getRowsByRange( + self._model, + row, + row, + num_row, + lower, + upper, + num_nz, + matrix_start, + matrix_index, + matrix_value, + ) ) return mip.xsum(matrix_value[i] * self.model.vars[i] for i in range(num_nz)) @@ -595,17 +620,19 @@ def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real: lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") - status = self._lib.Highs_getRowsByRange( - self._model, - idx, - idx, - num_row, - lower, - upper, - num_nz, - ffi.NULL, - ffi.NULL, - ffi.NULL, + check( + self._lib.Highs_getRowsByRange( + self._model, + idx, + idx, + num_row, + lower, + upper, + num_nz, + ffi.NULL, + ffi.NULL, + ffi.NULL, + ) ) # case distinction for sense @@ -622,17 +649,19 @@ def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") - status = self._lib.Highs_getRowsByRange( - self._model, - idx, - idx, - num_row, - lower, - upper, - num_nz, - ffi.NULL, - ffi.NULL, - ffi.NULL, + check( + self._lib.Highs_getRowsByRange( + self._model, + idx, + idx, + num_row, + lower, + upper, + num_nz, + ffi.NULL, + ffi.NULL, + ffi.NULL, + ) ) # update bounds as needed @@ -643,7 +672,7 @@ def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): ub = -rhs # set new bounds - status = self._lib.Highs_changeRowBounds(self._model, idx, lb, ub) + check(self._lib.Highs_changeRowBounds(self._model, idx, lb, ub)) def constr_get_name(self: "SolverHighs", idx: int) -> str: return self._cons_name(idx) @@ -657,7 +686,7 @@ def constr_get_slack(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: def remove_constrs(self: "SolverHighs", constrsList: List[int]): set_ = ffi.new("int[]", constrsList) - status = self._lib.Highs_deleteRowsBySet(self._model, len(constrsList), set_) + check(self._lib.Highs_deleteRowsBySet(self._model, len(constrsList), set_)) def constr_get_index(self: "SolverHighs", name: str) -> int: return self._cons_col(name) @@ -681,25 +710,27 @@ def var_get_lb(self: "SolverHighs", var: "mip.Var") -> numbers.Real: lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") - status = self._lib.Highs_getColsByRange( - self._model, - var.idx, # from_col - var.idx, # to_col - num_col, - costs, - lower, - upper, - num_nz, - ffi.NULL, # matrix_start - ffi.NULL, # matrix_index - ffi.NULL, # matrix_value + check( + self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) ) return lower[0] def var_set_lb(self: "SolverHighs", var: "mip.Var", value: numbers.Real): # can only set both bounds, so we just set the old upper bound old_upper = self.var_get_ub(var) - status = self._lib.Highs_changeColBounds(self._model, var.idx, value, old_upper) + check(self._lib.Highs_changeColBounds(self._model, var.idx, value, old_upper)) def var_get_ub(self: "SolverHighs", var: "mip.Var") -> numbers.Real: num_col = ffi.new("int*") @@ -707,25 +738,27 @@ def var_get_ub(self: "SolverHighs", var: "mip.Var") -> numbers.Real: lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") - status = self._lib.Highs_getColsByRange( - self._model, - var.idx, # from_col - var.idx, # to_col - num_col, - costs, - lower, - upper, - num_nz, - ffi.NULL, # matrix_start - ffi.NULL, # matrix_index - ffi.NULL, # matrix_value + check( + self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) ) return upper[0] def var_set_ub(self: "SolverHighs", var: "mip.Var", value: numbers.Real): # can only set both bounds, so we just set the old lower bound old_lower = self.var_get_lb(var) - status = self._lib.Highs_changeColBounds(self._model, var.idx, old_lower, value) + check(self._lib.Highs_changeColBounds(self._model, var.idx, old_lower, value)) def var_get_obj(self: "SolverHighs", var: "mip.Var") -> numbers.Real: num_col = ffi.new("int*") @@ -733,31 +766,35 @@ def var_get_obj(self: "SolverHighs", var: "mip.Var") -> numbers.Real: lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") - status = self._lib.Highs_getColsByRange( - self._model, - var.idx, # from_col - var.idx, # to_col - num_col, - costs, - lower, - upper, - num_nz, - ffi.NULL, # matrix_start - ffi.NULL, # matrix_index - ffi.NULL, # matrix_value + check( + self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) ) return costs[0] def var_set_obj(self: "SolverHighs", var: "mip.Var", value: numbers.Real): - status = self._lib.Highs_changeColCost(self._model, var.idx, value) + check(self._lib.Highs_changeColCost(self._model, var.idx, value)) def var_get_var_type(self: "SolverHighs", var: "mip.Var") -> str: return self._var_type[var.idx] def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str): if value != mip.CONTINUOUS: - status = self._lib.Highs_changeColIntegrality( - self._model, var.idx, self._lib.kHighsVarTypeInteger + check( + self._lib.Highs_changeColIntegrality( + self._model, var.idx, self._lib.kHighsVarTypeInteger + ) ) self._var_type[var.idx] = value @@ -769,35 +806,39 @@ def var_get_column(self: "SolverHighs", var: "mip.Var") -> "mip.Column": lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") - status = self._lib.Highs_getColsByRange( - self._model, - var.idx, # from_col - var.idx, # to_col - num_col, - costs, - lower, - upper, - num_nz, - ffi.NULL, # matrix_start - ffi.NULL, # matrix_index - ffi.NULL, # matrix_value + check( + self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + ffi.NULL, # matrix_start + ffi.NULL, # matrix_index + ffi.NULL, # matrix_value + ) ) # - second, to get the coefficients in pre-allocated arrays. matrix_start = ffi.new("int[]", 1) matrix_index = ffi.new("int[]", num_nz[0]) matrix_value = ffi.new("double[]", num_nz[0]) - status = self._lib.Highs_getColsByRange( - self._model, - var.idx, # from_col - var.idx, # to_col - num_col, - costs, - lower, - upper, - num_nz, - matrix_start, - matrix_index, - matrix_value, + check( + self._lib.Highs_getColsByRange( + self._model, + var.idx, # from_col + var.idx, # to_col + num_col, + costs, + lower, + upper, + num_nz, + matrix_start, + matrix_index, + matrix_value, + ) ) return mip.Column( @@ -810,7 +851,6 @@ def var_set_column(self: "SolverHighs", var: "mip.Var", value: "mip.Column"): raise NotImplementedError() def var_get_rc(self: "SolverHighs", var: "mip.Var") -> numbers.Real: - # TODO: double-check this! if self._rc: return self._rc[var.idx] @@ -826,7 +866,7 @@ def var_get_name(self: "SolverHighs", idx: int) -> str: def remove_vars(self: "SolverHighs", varsList: List[int]): set_ = ffi.new("int[]", varsList) - status = self._lib.Highs_deleteColsBySet(self._model, len(varsList), set_) + check(self._lib.Highs_deleteColsBySet(self._model, len(varsList), set_)) def var_get_index(self: "SolverHighs", name: str) -> int: return self._var_col[name] From b0c5066074fd2ec06cfccad34d2364923bc2a83e Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 16:40:54 +0100 Subject: [PATCH 35/65] highs: (untested) attempt to locate library for non-linux OSs --- mip/highs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 7aa3b5d0..ef2416ae 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -29,7 +29,12 @@ pkg_path = os.path.dirname(highspy.__file__) # need library matching operating system - if "linux" in sys.platform.lower(): + platform = sys.platform.lower() + if "linux" in platform: + pattern = "highs_bindings.*.so" + elif platform.startswith("win"): + pattern = "highs_bindings.*.pyd" + elif any(platform.startswith(p) for p in ("darwin", "macos")): pattern = "highs_bindings.*.so" else: raise NotImplementedError(f"{sys.platform} not supported!") From c83d8ca7201a3a16295d15d74e9c7310ad035649 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 17:05:39 +0100 Subject: [PATCH 36/65] highs: support changing columns --- mip/highs.py | 27 ++++++++++++++++++++++++--- test/test_model.py | 27 ++++++++++++++++++++------- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index ef2416ae..e55f7621 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -109,6 +109,9 @@ HighsInt Highs_changeColBounds( void* highs, const HighsInt col, const double lower, const double upper ); +HighsInt Highs_changeCoeff( + void* highs, const HighsInt row, const HighsInt col, const double value +); HighsInt Highs_getRowsByRange( const void* highs, const HighsInt from_row, const HighsInt to_row, HighsInt* num_row, double* lower, double* upper, HighsInt* num_nz, @@ -260,6 +263,23 @@ def _set_bool_option_value(self: "SolverHighs", name: str, value: float): self._lib.Highs_setBoolOptionValue(self._model, name.encode("UTF-8"), value) ) + def _change_coef(self: "SolverHighs", row: int, col: int, value: float): + "Overwrite a single coefficient in the matrix." + check(self._lib.Highs_changeCoeff(self._model, row, col, value)) + + def _set_column(self: "SolverHighs", col: int, column: "mip.Column"): + "Overwrite coefficients of one column." + # We also have to set to 0 all coefficients of the old column, so we + # fetch that first. + var = self.model.vars[col] + old_column = self.var_get_column(var) + coeffs = {cons.idx: 0.0 for cons in old_column.constrs} + coeffs.update( + {cons.idx: coef for cons, coef in zip(column.constrs, column.coeffs)} + ) + for row, coef in coeffs.items(): + self._change_coef(row, col, coef) + def add_var( self: "SolverHighs", obj: numbers.Real = 0, @@ -269,7 +289,6 @@ def add_var( column: "mip.Column" = None, name: str = "", ): - # TODO: handle column data col: int = self.num_cols() check(self._lib.Highs_addVar(self._model, lb, ub)) check(self._lib.Highs_changeColCost(self._model, col, obj)) @@ -280,6 +299,9 @@ def add_var( ) ) + if column: + self._set_column(col, column) + # store name & type self._var_name.append(name) self._var_col[name] = col @@ -852,8 +874,7 @@ def var_get_column(self: "SolverHighs", var: "mip.Var") -> "mip.Column": ) def var_set_column(self: "SolverHighs", var: "mip.Var", value: "mip.Column"): - # TODO - raise NotImplementedError() + self._set_column(var.idx, value) def var_get_rc(self: "SolverHighs", var: "mip.Var") -> numbers.Real: if self._rc: diff --git a/test/test_model.py b/test/test_model.py index 58d41e94..c1f58ff5 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -679,8 +679,12 @@ def test_setting_variable_attributes(solver): y = m.add_var("y", obj=1) c = m.add_constr(y <= x, "some_constraint") # TODO: Remove Not implemented error when implemented - with pytest.raises(NotImplementedError): - x.column = Column([c], [-2]) # new column based on constraint (y <= 2*x) + column = Column([c], [-2]) # new column based on constraint (y <= 2*x) + if solver == HIGHS: + x.column = column + else: + with pytest.raises(NotImplementedError): + x.column = column # Check before optimization assert x.lb == -1.0 @@ -693,15 +697,24 @@ def test_setting_variable_attributes(solver): if solver == CBC: assert x.branch_priority == 0 # TODO: Check when implemented - # column = x.column - # assert column.coeffs == [-2] - # assert column.constrs == [c] + if solver == HIGHS: + column = x.column + assert column.coeffs == [-2] + assert column.constrs == [c] m.optimize() # Check that optimization result considered changes correctly - assert abs(m.objective_value - 10.0) <= TOL - assert abs(x.x - 5) < TOL + if solver == HIGHS: + # column was changed, so y == 2*x + assert abs(m.objective_value - 15.0) <= TOL + assert abs(x.x - 5) < TOL + assert abs(y.x - 10) < TOL + else: + # column was not changed, so y == x + assert abs(m.objective_value - 10.0) <= TOL + assert abs(x.x - 5) < TOL + assert abs(y.x - 5) < TOL @pytest.mark.parametrize("solver", SOLVERS) From 7403f8f5a081d41b74437a80035c4160dfde1570 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 17:23:58 +0100 Subject: [PATCH 37/65] highs: support optimize(relax=True) --- mip/highs.py | 31 +++++++++++++++++++++++++++---- test/test_model.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index e55f7621..4a620974 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -382,8 +382,7 @@ def get_objective_const(self: "SolverHighs") -> numbers.Real: check(self._lib.Highs_getObjectiveOffset(self._model, offset)) return offset[0] - def relax(self: "SolverHighs"): - # change integrality of all columns + def _all_cols_continuous(self: "SolverHighs"): n = self.num_cols() integrality = ffi.new( "int[]", [self._lib.kHighsVarTypeContinuous for i in range(n)] @@ -393,6 +392,24 @@ def relax(self: "SolverHighs"): self._model, 0, n - 1, integrality ) ) + + def _reset_var_types(self: "SolverHighs"): + var_type_map = { + mip.CONTINUOUS: self._lib.kHighsVarTypeContinuous, + mip.BINARY: self._lib.kHighsVarTypeInteger, + mip.INTEGER: self._lib.kHighsVarTypeInteger, + } + integrality = ffi.new("int[]", [var_type_map[vt] for vt in self._var_type]) + n = self.num_cols() + check( + self._lib.Highs_changeColsIntegralityByRange( + self._model, 0, n - 1, integrality + ) + ) + + def relax(self: "SolverHighs"): + # change integrality of all columns + self._all_cols_continuous() self._var_type = [mip.CONTINUOUS] * len(self._var_type) def generate_cuts( @@ -413,8 +430,10 @@ def optimize( relax: bool = False, ) -> "mip.OptimizationStatus": if relax: - # TODO: handle relax (need to remember and reset integrality?! - raise NotImplementedError() + # Temporarily change variable types. Original types are still stored + # in self._var_type. + self._all_cols_continuous() + check(self._lib.Highs_run(self._model)) # store solution values for later access @@ -440,6 +459,10 @@ def optimize( if self._has_dual_solution(): self._pi = [row_dual[i] for i in range(m)] + if relax: + # Undo the temporary changes. + self._reset_var_types() + return opt_status def get_objective_value(self: "SolverHighs") -> numbers.Real: diff --git a/test/test_model.py b/test/test_model.py index c1f58ff5..c847126f 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -1360,3 +1360,31 @@ def test_change_objective_sense(solver): status = m.optimize() assert status == OptimizationStatus.OPTIMAL assert x.x == pytest.approx(10.0) + +@pytest.mark.parametrize("solver", SOLVERS) +def test_solve_relaxation(solver): + m = Model(solver_name=solver) + x = m.add_var("x", var_type=CONTINUOUS) + y = m.add_var("y", var_type=INTEGER) + z = m.add_var("z", var_type=BINARY) + + m.add_constr(x <= 10 * z) + m.add_constr(x <= 9.5) + m.add_constr(x + y <= 20) + m.objective = mip.maximize(4*x + y - z) + + # first solve proper MIP + status = m.optimize() + assert status == OptimizationStatus.OPTIMAL + assert x.x == pytest.approx(9.5) + assert y.x == pytest.approx(10.0) + assert z.x == pytest.approx(1.0) + + # then compare LP relaxation + # (seems to fail for CBC?!) + if solver == HIGHS: + status = m.optimize(relax=True) + assert status == OptimizationStatus.OPTIMAL + assert x.x == pytest.approx(9.5) + assert y.x == pytest.approx(10.5) + assert z.x == pytest.approx(0.95) From 0ebf32a0d81b21e6d28b87e9f8e2ea740e0648c5 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 17:39:59 +0100 Subject: [PATCH 38/65] highs: add messages to NotImplementedError --- mip/highs.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 4a620974..bc26e855 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -333,17 +333,17 @@ def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): self._cons_col[name] = row def add_lazy_constr(self: "SolverHighs", lin_expr: "mip.LinExpr"): - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support lazy constraints!") def add_sos( self: "SolverHighs", sos: List[Tuple["mip.Var", numbers.Real]], sos_type: int, ): - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support SOS!") def add_cut(self: "SolverHighs", lin_expr: "mip.LinExpr"): - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support cut callbacks!") def get_objective_bound(self: "SolverHighs") -> numbers.Real: return self._get_double_info_value("mip_dual_bound") @@ -420,10 +420,10 @@ def generate_cuts( max_cuts: int = mip.INT_MAX, min_viol: numbers.Real = 1e-4, ) -> "mip.CutPool": - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support manual cut generation.") def clique_merge(self, constrs: Optional[List["mip.Constr"]] = None): - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support clique merging!") def optimize( self: "SolverHighs", @@ -442,7 +442,6 @@ def optimize( mip.OptimizationStatus.OPTIMAL, mip.OptimizationStatus.FEASIBLE, ): - # TODO: also handle primal/dual rays? n, m = self.num_cols(), self.num_rows() col_value = ffi.new("double[]", n) col_dual = ffi.new("double[]", n) @@ -473,10 +472,10 @@ def get_objective_value(self: "SolverHighs") -> numbers.Real: def get_log( self: "SolverHighs", ) -> List[Tuple[numbers.Real, Tuple[numbers.Real, numbers.Real]]]: - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't give access to a progress log.") def get_objective_value_i(self: "SolverHighs", i: int) -> numbers.Real: - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't store multiple solutions.") def get_num_solutions(self: "SolverHighs") -> int: # Multiple solutions are not supported (through C API?). @@ -499,7 +498,7 @@ def set_objective_sense(self: "SolverHighs", sense: str): check(self._lib.Highs_changeObjectiveSense(self._model, sense_map[sense])) def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support a start solution.") def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = ""): # set coefficients @@ -545,10 +544,10 @@ def set_max_solutions(self: "SolverHighs", max_solutions: int): self._get_int_option_value("mip_max_improving_sols", max_solutions) def get_pump_passes(self: "SolverHighs") -> int: - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support pump passes.") def set_pump_passes(self: "SolverHighs", passes: int): - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support pump passes.") def get_max_nodes(self: "SolverHighs") -> int: return self._get_int_option_value("mip_max_nodes") @@ -584,10 +583,10 @@ def num_int(self: "SolverHighs") -> int: return sum(vt != mip.CONTINUOUS for vt in self._var_type) def get_emphasis(self: "SolverHighs") -> mip.SearchEmphasis: - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support search emphasis.") def set_emphasis(self: "SolverHighs", emph: mip.SearchEmphasis): - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support search emphasis.") def get_cutoff(self: "SolverHighs") -> numbers.Real: return self._get_double_option_value("objective_bound") @@ -908,7 +907,7 @@ def var_get_x(self: "SolverHighs", var: "mip.Var") -> numbers.Real: return self._x[var.idx] def var_get_xi(self: "SolverHighs", var: "mip.Var", i: int) -> numbers.Real: - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't store multiple solutions.") def var_get_name(self: "SolverHighs", idx: int) -> str: return self._var_name[idx] @@ -971,7 +970,7 @@ def get_status(self: "SolverHighs") -> mip.OptimizationStatus: def cgraph_density(self: "SolverHighs") -> float: """Density of the conflict graph""" - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support conflict graph.") def conflicting( self: "SolverHighs", @@ -980,7 +979,7 @@ def conflicting( ) -> bool: """Checks if two assignment to binary variables are in conflict, returns none if no conflict graph is available""" - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support conflict graph.") def conflicting_nodes( self: "SolverHighs", v1: Union["mip.Var", "mip.LinExpr"] @@ -988,10 +987,10 @@ def conflicting_nodes( """Returns all assignment conflicting with the assignment in v1 in the conflict graph. """ - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support conflict graph.") def feature_values(self: "SolverHighs") -> List[float]: - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support feature extraction.") def feature_names(self: "SolverHighs") -> List[str]: - raise NotImplementedError() + raise NotImplementedError("HiGHS doesn't support feature extraction.") From 42f53869e62e1b08bcad3451cbc9a05dfae1734b Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 8 Feb 2023 17:46:31 +0100 Subject: [PATCH 39/65] WIP: properly handle cons_{get,set}_expr and slack --- mip/highs.py | 81 +++++++++++++++++++++++++++++++++++----------- test/test_model.py | 21 ++++++++++-- 2 files changed, 80 insertions(+), 22 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index bc26e855..f69bdb48 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -638,30 +638,61 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": ) # - second, to get the coefficients in pre-allocated arrays. - matrix_start = ffi.new("int[]", 1) - matrix_index = ffi.new("int[]", num_nz[0]) - matrix_value = ffi.new("double[]", num_nz[0]) - check( - self._lib.Highs_getRowsByRange( - self._model, - row, - row, - num_row, - lower, - upper, - num_nz, - matrix_start, - matrix_index, - matrix_value, + if num_nz[0] == 0: + # early exit for empty expressions + expr = mip.xsum([]) + else: + matrix_start = ffi.new("int[]", 1) + matrix_index = ffi.new("int[]", num_nz[0]) + matrix_value = ffi.new("double[]", num_nz[0]) + check( + self._lib.Highs_getRowsByRange( + self._model, + row, + row, + num_row, + lower, + upper, + num_nz, + matrix_start, + matrix_index, + matrix_value, + ) + ) + expr = mip.xsum( + matrix_value[i] * self.model.vars[i] for i in range(num_nz[0]) ) - ) - return mip.xsum(matrix_value[i] * self.model.vars[i] for i in range(num_nz)) + # Also set sense and constant + lhs, rhs = lower[0], upper[0] + if rhs < mip.INF: + expr -= rhs + if lhs > -mip.INF: + assert lhs == rhs + expr.sense = mip.EQUAL + else: + expr.sense = mip.LESS_OR_EQUAL + else: + if lhs > -mip.INF: + expr -= lhs + expr.sense = mip.GREATER_OR_EQUAL + else: + raise ValueError("Unbounded constraint?!") + return expr def constr_set_expr( self: "SolverHighs", constr: "mip.Constr", value: "mip.LinExpr" ) -> "mip.LinExpr": - raise NotImplementedError() + # We also have to set to 0 all coefficients of the old row, so we + # fetch that first. + coeffs = {var: 0.0 for var in constr.expr} + + # Then we fetch the new coefficients and overwrite. + coeffs.update(value.expr.items()) + + # Finally, we change the coeffs in HiGHS' matrix one-by-one. + for var, coef in coeffs.items(): + self._change_coef(constr.idx, var.idx, coef) def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real: # fetch both lower and upper bound @@ -731,7 +762,19 @@ def constr_get_pi(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: return self._pi[constr.idx] def constr_get_slack(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: - raise NotImplementedError() + expr = constr.expr + activity = sum(coef * var.x for var, coef in expr.expr.items()) + rhs = -expr.const + slack = rhs - activity + assert False + if expr.sense == mip.LESS_OR_EQUAL: + return slack + elif expr.sense == mip.GREATER_OR_EQUAL: + return -slack + elif expr.sense == mip.EQUAL: + return -abs(slack) + else: + raise ValueError(f"Invalid constraint sense: {expr.sense}") def remove_constrs(self: "SolverHighs", constrsList: List[int]): set_ = ffi.new("int[]", constrsList) diff --git a/test/test_model.py b/test/test_model.py index c847126f..ff2849fb 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -1368,11 +1368,18 @@ def test_solve_relaxation(solver): y = m.add_var("y", var_type=INTEGER) z = m.add_var("z", var_type=BINARY) - m.add_constr(x <= 10 * z) - m.add_constr(x <= 9.5) - m.add_constr(x + y <= 20) + c1 = m.add_constr(x <= 10 * z) + c2 = m.add_constr(x <= 9.5) + c3 = m.add_constr(x + y <= 20) m.objective = mip.maximize(4*x + y - z) + # double-check constraint expressions + assert c1.idx == 0 + expr1 = c1.expr + assert expr1.expr == pytest.approx({x: 1.0, z: -10.0}) + assert expr1.const == pytest.approx(0.0) + assert expr1.sense == mip.LESS_OR_EQUAL + # first solve proper MIP status = m.optimize() assert status == OptimizationStatus.OPTIMAL @@ -1380,6 +1387,10 @@ def test_solve_relaxation(solver): assert y.x == pytest.approx(10.0) assert z.x == pytest.approx(1.0) + assert c1.slack == pytest.approx(0.5) + assert c2.slack == pytest.approx(0.0) + assert c3.slack == pytest.approx(0.0) + # then compare LP relaxation # (seems to fail for CBC?!) if solver == HIGHS: @@ -1388,3 +1399,7 @@ def test_solve_relaxation(solver): assert x.x == pytest.approx(9.5) assert y.x == pytest.approx(10.5) assert z.x == pytest.approx(0.95) + + assert c1.slack == pytest.approx(0.0) + assert c2.slack == pytest.approx(0.0) + assert c3.slack == pytest.approx(0.0) From 5e804e7812f3b54d56fc38ca5cc79e529760233f Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 9 Feb 2023 09:47:08 +0100 Subject: [PATCH 40/65] don't use highslib if not available --- mip/highs.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index f69bdb48..475579a7 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -167,13 +167,12 @@ if has_highs: ffi.cdef(HEADER) -STATUS_ERROR = highslib.kHighsStatusError + STATUS_ERROR = highslib.kHighsStatusError - -def check(status): - "Check return status and raise error if not OK." - if status == STATUS_ERROR: - raise mip.InterfacingError("Unknown error in call to HiGHS.") + def check(status): + "Check return status and raise error if not OK." + if status == STATUS_ERROR: + raise mip.InterfacingError("Unknown error in call to HiGHS.") class SolverHighs(mip.Solver): From fc53603847d42b37fb5903e9da33464ad5d14963 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 10 Feb 2023 09:50:39 +0100 Subject: [PATCH 41/65] remove debugging assert --- mip/highs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 475579a7..c76bbccf 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -765,7 +765,6 @@ def constr_get_slack(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: activity = sum(coef * var.x for var, coef in expr.expr.items()) rhs = -expr.const slack = rhs - activity - assert False if expr.sense == mip.LESS_OR_EQUAL: return slack elif expr.sense == mip.GREATER_OR_EQUAL: From 7a2624389ed6dfe0fc7307f6375fd0085447b6d3 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 10 Feb 2023 09:50:46 +0100 Subject: [PATCH 42/65] highs: fix constr_get_expr (use actual column indices) --- mip/highs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index c76bbccf..069c2323 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -659,7 +659,8 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": ) ) expr = mip.xsum( - matrix_value[i] * self.model.vars[i] for i in range(num_nz[0]) + matrix_value[i] * self.model.vars[matrix_index[i]] + for i in range(num_nz[0]) ) # Also set sense and constant From d987a819736ba1480811f40a87f7adf9877b8712 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 10 Feb 2023 09:52:07 +0100 Subject: [PATCH 43/65] add work-around problem in HiGHS' C API. --- mip/highs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 069c2323..415c9e92 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -621,6 +621,9 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": lower = ffi.new("double[]", 1) upper = ffi.new("double[]", 1) num_nz = ffi.new("int*") + # TODO: We also pass a non-NULL matrix_start, which should not be + # needed, but works around a known bug in HiGHS' C API. + _tmp_matrix_start = ffi.new("int[]", 1) check( self._lib.Highs_getRowsByRange( self._model, @@ -630,7 +633,7 @@ def constr_get_expr(self: "SolverHighs", constr: "mip.Constr") -> "mip.LinExpr": lower, upper, num_nz, - ffi.NULL, + _tmp_matrix_start, ffi.NULL, ffi.NULL, ) From 02141368f1a418ab6e5854b09c1729c94d01a2ff Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Fri, 10 Feb 2023 09:55:26 +0100 Subject: [PATCH 44/65] fix test (had incorrect assumptions) --- test/test_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_model.py b/test/test_model.py index ff2849fb..e04f32c9 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -1375,7 +1375,7 @@ def test_solve_relaxation(solver): # double-check constraint expressions assert c1.idx == 0 - expr1 = c1.expr + expr1 = c1.expr # store to avoid repeated calls assert expr1.expr == pytest.approx({x: 1.0, z: -10.0}) assert expr1.const == pytest.approx(0.0) assert expr1.sense == mip.LESS_OR_EQUAL @@ -1389,7 +1389,7 @@ def test_solve_relaxation(solver): assert c1.slack == pytest.approx(0.5) assert c2.slack == pytest.approx(0.0) - assert c3.slack == pytest.approx(0.0) + assert c3.slack == pytest.approx(0.5) # then compare LP relaxation # (seems to fail for CBC?!) From a9e6a1f312e0ea205cc6ea8f97d6e61bc01bce90 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 19:18:07 +0100 Subject: [PATCH 45/65] pyproject.toml: add highspy as optional dependency --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b618017d..f3fdaeb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dependencies = ["cffi==1.15.*"] [project.optional-dependencies] numpy = ["numpy==1.24.*; python_version >= '3.8'", "numpy==1.21.6; python_version == '3.7'"] gurobi = ["gurobipy>=8"] +highs = ["highspy>=1.5"] test = ["pytest==7.2.0", "networkx==2.8.8; python_version >= '3.8'", "networkx==2.6.3; python_version == '3.7'", "matplotlib==3.6.2; python_version >= '3.8'", "matplotlib==3.5.3; python_version == '3.7'"] [project.urls] @@ -50,4 +51,4 @@ packages = ["mip"] "mip.libraries" = ["*.so", "*.dylib", "*.dll"] [tool.setuptools_scm] -write_to = "mip/_version.py" \ No newline at end of file +write_to = "mip/_version.py" From 3261dae350b063817f18ed5a1528f59853ffe4a7 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 19:20:46 +0100 Subject: [PATCH 46/65] include highs in GitHub CI (try for 1st time) --- .github/workflows/github-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-ci.yml b/.github/workflows/github-ci.yml index 1068faed..d543a95f 100644 --- a/.github/workflows/github-ci.yml +++ b/.github/workflows/github-ci.yml @@ -87,7 +87,7 @@ jobs: - name: Install mip for testing (CPython) if: ${{ matrix.python-version != 'pypy3.9-v7.3.9' }} - run: python -m pip install .[test,numpy,gurobi] + run: python -m pip install .[test,numpy,gurobi,highs] - name: list installed packages run: python -m pip list From 8918708369d0f4c61a5767eb25bfa8c2ee53c3fa Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 19:29:49 +0100 Subject: [PATCH 47/65] include HiGHS as solvers in other (slow) tests --- test/dbft_test.py | 4 ++++ test/examples_test.py | 4 ++++ test/mip_files_test.py | 5 ++++- test/mip_test.py | 5 ++++- test/rcpsp_test.py | 5 ++++- test/two_dim_pack_test.py | 5 ++++- 6 files changed, 24 insertions(+), 4 deletions(-) diff --git a/test/dbft_test.py b/test/dbft_test.py index 1f5067f9..52bf488a 100644 --- a/test/dbft_test.py +++ b/test/dbft_test.py @@ -4,6 +4,7 @@ from os import environ from itertools import product import pytest +import mip.highs from mip import ( Model, BINARY, @@ -13,6 +14,7 @@ maximize, CBC, GUROBI, + HIGHS, ) TOL = 1e-4 @@ -20,6 +22,8 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] # for each pair o N, tMax, the expected: (optimal, columns, rows, non-zeros) PDATA = { diff --git a/test/examples_test.py b/test/examples_test.py index 9dd0afae..2f7388e0 100644 --- a/test/examples_test.py +++ b/test/examples_test.py @@ -8,12 +8,16 @@ import importlib.machinery import pytest +import mip.highs + EXAMPLES = glob(join("..", "examples", "*.py")) + glob(join(".", "examples", "*.py")) SOLVERS = ["cbc"] if "GUROBI_HOME" in environ: SOLVERS += ["gurobi"] +if mip.highs.has_highs: + SOLVERS += ["HiGHS"] @pytest.mark.parametrize("solver, example", product(SOLVERS, EXAMPLES)) diff --git a/test/mip_files_test.py b/test/mip_files_test.py index a5a34c1d..642cf526 100644 --- a/test/mip_files_test.py +++ b/test/mip_files_test.py @@ -7,7 +7,8 @@ from itertools import product import pytest import mip -from mip import Model, OptimizationStatus, GUROBI, CBC +import mip.highs +from mip import Model, OptimizationStatus, GUROBI, CBC, HIGHS # for each MIP in test/data, best lower and upper bounds # to be used when checking optimization results @@ -89,6 +90,8 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] # check availability of test data DATA_DIR = join(join(dirname(mip.__file__)[0:-3], "test"), "data") diff --git a/test/mip_test.py b/test/mip_test.py index 06ea3d37..65c8e133 100644 --- a/test/mip_test.py +++ b/test/mip_test.py @@ -2,8 +2,9 @@ from itertools import product import pytest import networkx as nx +import mip.highs from mip import Model, xsum, OptimizationStatus, MAXIMIZE, BINARY, INTEGER -from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, Column +from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, HIGHS, Column from os import environ import math @@ -12,6 +13,8 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] @pytest.mark.parametrize("solver", SOLVERS) diff --git a/test/rcpsp_test.py b/test/rcpsp_test.py index 812e82e4..fed83010 100644 --- a/test/rcpsp_test.py +++ b/test/rcpsp_test.py @@ -5,7 +5,8 @@ import json from itertools import product import pytest -from mip import CBC, GUROBI, OptimizationStatus +import mip.highs +from mip import CBC, GUROBI, HIGHS, OptimizationStatus from mip_rcpsp import create_mip INSTS = glob("./data/rcpsp*.json") + glob("./test/data/rcpsp*.json") @@ -15,6 +16,8 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) diff --git a/test/two_dim_pack_test.py b/test/two_dim_pack_test.py index ce3f3469..c07a8c47 100644 --- a/test/two_dim_pack_test.py +++ b/test/two_dim_pack_test.py @@ -5,7 +5,8 @@ import json from itertools import product import pytest -from mip import CBC, GUROBI, OptimizationStatus +import mip.highs +from mip import CBC, GUROBI, HIGHS, OptimizationStatus from mip_2d_pack import create_mip INSTS = glob("./data/two_dim_pack_p*.json") + glob( @@ -17,6 +18,8 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) From a23d1681a3dd833f33499d7ab6dc706d4c13e144 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 20:09:53 +0100 Subject: [PATCH 48/65] Fix: Don't use self._set_column when adding variables. This lead to index error in example cuttingstock_cg.py, because _set_column uses the mip.Var object which is not yet added to the mip.Model. Instead, manually set the coefficients. --- mip/highs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 415c9e92..b4232d1e 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -299,7 +299,11 @@ def add_var( ) if column: - self._set_column(col, column) + # Can't use _set_column here, because the variable is not added to + # the mip.Model yet. + # self._set_column(col, column) + for cons, coef in zip(column.constrs, column.coeffs): + self._change_coef(cons.idx, col, coef) # store name & type self._var_name.append(name) From 1dcaae11e386e6d8dea1de67da210bdb4614cf0a Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 20:12:46 +0100 Subject: [PATCH 49/65] Fix: use brackets to index into dicts/lists --- mip/highs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index b4232d1e..eb5857ad 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -762,7 +762,7 @@ def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): check(self._lib.Highs_changeRowBounds(self._model, idx, lb, ub)) def constr_get_name(self: "SolverHighs", idx: int) -> str: - return self._cons_name(idx) + return self._cons_name[idx] def constr_get_pi(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: if self._pi: @@ -787,7 +787,7 @@ def remove_constrs(self: "SolverHighs", constrsList: List[int]): check(self._lib.Highs_deleteRowsBySet(self._model, len(constrsList), set_)) def constr_get_index(self: "SolverHighs", name: str) -> int: - return self._cons_col(name) + return self._cons_col[name] # Variable-related getters/setters From 4e2dc15a59246c58a97b42a6e6fa83b33cb54f9d Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 21:06:12 +0100 Subject: [PATCH 50/65] Fix: add missing Highs_changeRowBounds to CFFI header --- mip/highs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mip/highs.py b/mip/highs.py index eb5857ad..dda96256 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -112,6 +112,9 @@ HighsInt Highs_changeCoeff( void* highs, const HighsInt row, const HighsInt col, const double value ); +HighsInt Highs_changeRowBounds( + void* highs, const HighsInt row, const double lower, const double upper +); HighsInt Highs_getRowsByRange( const void* highs, const HighsInt from_row, const HighsInt to_row, HighsInt* num_row, double* lower, double* upper, HighsInt* num_nz, From 62eed996c46423cea44124def494c7cd36855781 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 21:06:51 +0100 Subject: [PATCH 51/65] Fix: Don't negate rhs in constraint properties (getter/setter) This is different from the "constant" in Constraint. --- mip/highs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index dda96256..605eaf1e 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -727,11 +727,11 @@ def constr_get_rhs(self: "SolverHighs", idx: int) -> numbers.Real: # case distinction for sense if lower[0] == -mip.INF: - return -upper[0] + return upper[0] if upper[0] == mip.INF: - return -lower[0] + return lower[0] assert lower[0] == upper[0] - return -lower[0] + return lower[0] def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): # first need to figure out which bound to change (lower or upper) @@ -757,9 +757,9 @@ def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): # update bounds as needed lb, ub = lower[0], upper[0] if lb != -mip.INF: - lb = -rhs + lb = rhs if ub != mip.INF: - ub = -rhs + ub = rhs # set new bounds check(self._lib.Highs_changeRowBounds(self._model, idx, lb, ub)) From f6c95a9c3d82763e90fe3e738441453c840bd029 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 21:09:24 +0100 Subject: [PATCH 52/65] Fix: use set, not get for set_max_solutions --- mip/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mip/highs.py b/mip/highs.py index 605eaf1e..94f5b964 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -547,7 +547,7 @@ def get_max_solutions(self: "SolverHighs") -> int: return self._get_int_option_value("mip_max_improving_sols") def set_max_solutions(self: "SolverHighs", max_solutions: int): - self._get_int_option_value("mip_max_improving_sols", max_solutions) + self._set_int_option_value("mip_max_improving_sols", max_solutions) def get_pump_passes(self: "SolverHighs") -> int: raise NotImplementedError("HiGHS doesn't support pump passes.") From bfbcbea6d08f17fc12a9dcd9edb8cc27f3034100 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 21:12:58 +0100 Subject: [PATCH 53/65] Fix test: actually use specific solver, not default (Gurobi) --- test/mip_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mip_test.py b/test/mip_test.py index 65c8e133..6762e49f 100644 --- a/test/mip_test.py +++ b/test/mip_test.py @@ -612,7 +612,7 @@ def test_linexpr_x(solver: str, val: int): @pytest.mark.parametrize("solver", SOLVERS) def test_add_column(solver: str): """Simple test which add columns in a specific way""" - m = Model() + m = Model(solver_name=solver) x = m.add_var() example_constr1 = m.add_constr(x >= 1, "constr1") From d01a239d7186a50a5c7f6738053c53a26e8853f1 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Tue, 14 Mar 2023 21:19:58 +0100 Subject: [PATCH 54/65] Disable some tests that HiGHS can not pass. Some tests can't be used because HiGHS doesn't support some required feature, eg cut generation, lazy constraints or MIP start. This affects many of the examples (so we exclude all now) and also the tests "rcpsp" and "two_dim_pack". Some others fail because the current version of HiGHS doesn't allow querying the variable type. So we have to keep track of that in the wrapper, which only works when the model is built in Python, but not when read from a file, ie in the tests "mip_files". Finally, some examples yielded a parse error, eg "1443_0-9.lp". Was: Revert "include HiGHS as solvers in other (slow) tests". This (partially) reverts commit 8918708369d0f4c61a5767eb25bfa8c2ee53c3fa. --- test/examples_test.py | 4 ---- test/mip_files_test.py | 5 +---- test/mip_test.py | 4 +++- test/rcpsp_test.py | 5 +---- test/two_dim_pack_test.py | 5 +---- 5 files changed, 6 insertions(+), 17 deletions(-) diff --git a/test/examples_test.py b/test/examples_test.py index 2f7388e0..9dd0afae 100644 --- a/test/examples_test.py +++ b/test/examples_test.py @@ -8,16 +8,12 @@ import importlib.machinery import pytest -import mip.highs - EXAMPLES = glob(join("..", "examples", "*.py")) + glob(join(".", "examples", "*.py")) SOLVERS = ["cbc"] if "GUROBI_HOME" in environ: SOLVERS += ["gurobi"] -if mip.highs.has_highs: - SOLVERS += ["HiGHS"] @pytest.mark.parametrize("solver, example", product(SOLVERS, EXAMPLES)) diff --git a/test/mip_files_test.py b/test/mip_files_test.py index 642cf526..a5a34c1d 100644 --- a/test/mip_files_test.py +++ b/test/mip_files_test.py @@ -7,8 +7,7 @@ from itertools import product import pytest import mip -import mip.highs -from mip import Model, OptimizationStatus, GUROBI, CBC, HIGHS +from mip import Model, OptimizationStatus, GUROBI, CBC # for each MIP in test/data, best lower and upper bounds # to be used when checking optimization results @@ -90,8 +89,6 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] -if mip.highs.has_highs: - SOLVERS += [HIGHS] # check availability of test data DATA_DIR = join(join(dirname(mip.__file__)[0:-3], "test"), "data") diff --git a/test/mip_test.py b/test/mip_test.py index 6762e49f..56d3bb9a 100644 --- a/test/mip_test.py +++ b/test/mip_test.py @@ -383,7 +383,9 @@ def test_tsp_cuts(solver: str): assert abs(m.objective_value - 262) <= TOL # "mip model objective" -@pytest.mark.parametrize("solver", SOLVERS) +# Exclude HiGHS solver, which doesn't support MIP start. +SOLVERS_WITH_MIPSTART = [s for s in SOLVERS if s != HIGHS] +@pytest.mark.parametrize("solver", SOLVERS_WITH_MIPSTART) def test_tsp_mipstart(solver: str): """tsp related tests""" N = ["a", "b", "c", "d", "e", "f", "g"] diff --git a/test/rcpsp_test.py b/test/rcpsp_test.py index fed83010..812e82e4 100644 --- a/test/rcpsp_test.py +++ b/test/rcpsp_test.py @@ -5,8 +5,7 @@ import json from itertools import product import pytest -import mip.highs -from mip import CBC, GUROBI, HIGHS, OptimizationStatus +from mip import CBC, GUROBI, OptimizationStatus from mip_rcpsp import create_mip INSTS = glob("./data/rcpsp*.json") + glob("./test/data/rcpsp*.json") @@ -16,8 +15,6 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] -if mip.highs.has_highs: - SOLVERS += [HIGHS] @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) diff --git a/test/two_dim_pack_test.py b/test/two_dim_pack_test.py index c07a8c47..ce3f3469 100644 --- a/test/two_dim_pack_test.py +++ b/test/two_dim_pack_test.py @@ -5,8 +5,7 @@ import json from itertools import product import pytest -import mip.highs -from mip import CBC, GUROBI, HIGHS, OptimizationStatus +from mip import CBC, GUROBI, OptimizationStatus from mip_2d_pack import create_mip INSTS = glob("./data/two_dim_pack_p*.json") + glob( @@ -18,8 +17,6 @@ SOLVERS = [CBC] if "GUROBI_HOME" in environ: SOLVERS += [GUROBI] -if mip.highs.has_highs: - SOLVERS += [HIGHS] @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) From f425cb35dd0ed3ddcdd6532c0186e31493901230 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 15 Mar 2023 10:58:44 +0100 Subject: [PATCH 55/65] Fix available highspy version in pyproject.toml (with dev0 suffix) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f3fdaeb8..0811dad1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = ["cffi==1.15.*"] [project.optional-dependencies] numpy = ["numpy==1.24.*; python_version >= '3.8'", "numpy==1.21.6; python_version == '3.7'"] gurobi = ["gurobipy>=8"] -highs = ["highspy>=1.5"] +highs = ["highspy>=1.5.0dev"] test = ["pytest==7.2.0", "networkx==2.8.8; python_version >= '3.8'", "networkx==2.6.3; python_version == '3.7'", "matplotlib==3.6.2; python_version >= '3.8'", "matplotlib==3.5.3; python_version == '3.7'"] [project.urls] From 9082252dfc5cebfacc9dd170049c45ab7441660d Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Wed, 15 Mar 2023 11:10:33 +0100 Subject: [PATCH 56/65] Fix highspy version in pyproject.toml (again) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0811dad1..2a836992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = ["cffi==1.15.*"] [project.optional-dependencies] numpy = ["numpy==1.24.*; python_version >= '3.8'", "numpy==1.21.6; python_version == '3.7'"] gurobi = ["gurobipy>=8"] -highs = ["highspy>=1.5.0dev"] +highs = ["highspy>=1.5.0dev0"] test = ["pytest==7.2.0", "networkx==2.8.8; python_version >= '3.8'", "networkx==2.6.3; python_version == '3.7'", "matplotlib==3.6.2; python_version >= '3.8'", "matplotlib==3.5.3; python_version == '3.7'"] [project.urls] From c5fb0e55a30de5f815e7a8dc60d6f68757aa761d Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Mon, 22 May 2023 10:56:34 +0200 Subject: [PATCH 57/65] pyproject: upgrade highspy to 1.5.3 This should help with installation on Windows and maybe also Python 3.11. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2a836992..00f7a3aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = ["cffi==1.15.*"] [project.optional-dependencies] numpy = ["numpy==1.24.*; python_version >= '3.8'", "numpy==1.21.6; python_version == '3.7'"] gurobi = ["gurobipy>=8"] -highs = ["highspy>=1.5.0dev0"] +highs = ["highspy>=1.5.3"] test = ["pytest==7.2.0", "networkx==2.8.8; python_version >= '3.8'", "networkx==2.6.3; python_version == '3.7'", "matplotlib==3.6.2; python_version >= '3.8'", "matplotlib==3.5.3; python_version == '3.7'"] [project.urls] From 9945ed1879cedb990c43eba2b37893c85b308d19 Mon Sep 17 00:00:00 2001 From: Robert Schwarz Date: Thu, 15 Jun 2023 15:44:42 +0200 Subject: [PATCH 58/65] GitHub actions: install highspy for pypy --- .github/workflows/github-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-ci.yml b/.github/workflows/github-ci.yml index d543a95f..a94ad94c 100644 --- a/.github/workflows/github-ci.yml +++ b/.github/workflows/github-ci.yml @@ -83,7 +83,7 @@ jobs: - name: Install mip for testing (PyPy) if: ${{ matrix.python-version == 'pypy3.9-v7.3.9' }} - run: python -m pip install .[test,numpy] + run: python -m pip install .[test,numpy,highs] - name: Install mip for testing (CPython) if: ${{ matrix.python-version != 'pypy3.9-v7.3.9' }} From 8b917fc17cc0a919ccda6be29fb60b7f94ae65a4 Mon Sep 17 00:00:00 2001 From: Robert Schwarz <43053+rschwarz@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:25:38 +0200 Subject: [PATCH 59/65] Set gap limits in optimize() Co-authored-by: kbrix2000 <75687810+kbrix2000@users.noreply.github.com> --- mip/highs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mip/highs.py b/mip/highs.py index 94f5b964..78252e7c 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -440,6 +440,9 @@ def optimize( # in self._var_type. self._all_cols_continuous() + self.set_mip_gap(self.model.max_mip_gap) + self.set_mip_gap_abs(self.model.max_mip_gap_abs) + check(self._lib.Highs_run(self._model)) # store solution values for later access From fd27995cedaeb454a92d7f27b2f49a5863a8bc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Toffolo?= Date: Fri, 19 Jan 2024 17:45:28 -0800 Subject: [PATCH 60/65] Add warm start capability to HIGHS --- mip/highs.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 78252e7c..08076ea0 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -3,7 +3,6 @@ import glob import numbers import logging -import os import os.path import sys from typing import Dict, List, Optional, Tuple, Union @@ -159,6 +158,10 @@ const void* highs, double* col_value, double* col_dual, double* row_value, double* row_dual ); +HighsInt Highs_setSolution( + const void* highs, double* col_value, double* row_value, + double* col_dual, double* row_dual +); HighsInt Highs_deleteRowsBySet( void* highs, const HighsInt num_set_entries, const HighsInt* set ); @@ -507,7 +510,13 @@ def set_objective_sense(self: "SolverHighs", sense: str): check(self._lib.Highs_changeObjectiveSense(self._model, sense_map[sense])) def set_start(self: "SolverHighs", start: List[Tuple["mip.Var", numbers.Real]]): - raise NotImplementedError("HiGHS doesn't support a start solution.") + # using zeros for unset variables + nvars = len(self.model.vars) + cval = ffi.new("double[]", [0.0 for _ in range(nvars)]) + for col in start: + cval[col[0].idx] = col[1] + + self._lib.Highs_setSolution(self._model, cval, ffi.NULL, ffi.NULL, ffi.NULL) def set_objective(self: "SolverHighs", lin_expr: "mip.LinExpr", sense: str = ""): # set coefficients From e5fec986e795d41130dd98b1fda6a2db299eccdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Toffolo?= Date: Thu, 15 Feb 2024 21:44:28 -0800 Subject: [PATCH 61/65] Add HIGHS to tests and ignore those which fail due to unavailable features --- mip/__init__.py | 6 +++++- mip/gurobi.py | 7 ++++--- mip/model.py | 11 +++++++---- test/examples_test.py | 16 ++++++++++++---- test/test_gurobi.py | 7 ++++--- 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/mip/__init__.py b/mip/__init__.py index 3a78d55d..154dd1a1 100644 --- a/mip/__init__.py +++ b/mip/__init__.py @@ -7,6 +7,10 @@ from mip.ndarray import LinExprTensor from mip.entities import Column, Constr, LinExpr, Var, ConflictGraph from mip.model import * -from mip._version import __version__ + +try: + from ._version import __version__ +except ImportError: + __version__ = "unknown" name = "mip" diff --git a/mip/gurobi.py b/mip/gurobi.py index c7adcfaa..78c53f35 100644 --- a/mip/gurobi.py +++ b/mip/gurobi.py @@ -51,6 +51,7 @@ MAX_NAME_SIZE = 512 # for variables and constraints lib_path = None +has_gurobi = False if "GUROBI_HOME" in environ: if platform.lower().startswith("win"): @@ -93,9 +94,9 @@ if lib_path is None: - found = False + has_gurobi = False else: - found = True + has_gurobi = True grblib = ffi.dlopen(lib_path) ffi.cdef( @@ -339,7 +340,7 @@ class SolverGurobi(Solver): def __init__(self, model: Model, name: str, sense: str, modelp: CData = ffi.NULL): """modelp should be informed if a model should not be created, but only allow access to an existing one""" - if not found: + if not has_gurobi: raise FileNotFoundError( """Gurobi not found. Plase check if the Gurobi dynamic loadable library is reachable or define diff --git a/mip/model.py b/mip/model.py index 1fba51a0..3ed10ce3 100644 --- a/mip/model.py +++ b/mip/model.py @@ -4,7 +4,11 @@ from typing import List, Tuple, Optional, Union, Dict, Any import numbers import mip -from ._version import __version__ + +try: + from ._version import __version__ +except ImportError: + __version__ = "unknown" logger = logging.getLogger(__name__) @@ -94,8 +98,7 @@ def __init__( else: import mip.gurobi - if mip.gurobi.found: - + if mip.gurobi.has_gurobi: self.solver = mip.gurobi.SolverGurobi(self, name, sense) self.solver_name = mip.GUROBI else: @@ -406,7 +409,7 @@ def clear(self: "Model"): # checking which solvers are available import mip.gurobi - if mip.gurobi.found: + if mip.gurobi.has_gurobi: self.solver = mip.gurobi.SolverGurobi(self, self.name, sense) self.solver_name = mip.GUROBI else: diff --git a/test/examples_test.py b/test/examples_test.py index 9dd0afae..7c6a4495 100644 --- a/test/examples_test.py +++ b/test/examples_test.py @@ -8,12 +8,17 @@ import importlib.machinery import pytest +import mip.gurobi +import mip.highs +from mip import CBC, GUROBI, HIGHS EXAMPLES = glob(join("..", "examples", "*.py")) + glob(join(".", "examples", "*.py")) -SOLVERS = ["cbc"] -if "GUROBI_HOME" in environ: - SOLVERS += ["gurobi"] +SOLVERS = [CBC] +if mip.gurobi.has_gurobi: + SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] @pytest.mark.parametrize("solver, example", product(SOLVERS, EXAMPLES)) @@ -22,4 +27,7 @@ def test_examples(solver, example): environ["SOLVER_NAME"] = solver loader = importlib.machinery.SourceFileLoader("example", example) mod = types.ModuleType(loader.name) - loader.exec_module(mod) + try: + loader.exec_module(mod) + except NotImplementedError as e: + print("Skipping test for example '{}': {}".format(example, e)) diff --git a/test/test_gurobi.py b/test/test_gurobi.py index 6a2c0351..b5cb1e8d 100644 --- a/test/test_gurobi.py +++ b/test/test_gurobi.py @@ -1,12 +1,13 @@ import pytest import mip +import mip.gurobi def test_gurobi_pip_installation(): # Even though we have no valid license yet, we could check that the binaries are found. # If no valid license is found, an InterfacingError is thrown - with pytest.raises(mip.InterfacingError): - mip.Model(solver_name="GRB") - + if mip.gurobi.has_gurobi: + with pytest.raises(mip.InterfacingError): + mip.Model(solver_name=mip.GUROBI) From 775b6e19ac89c72759b976816069d23318c6334b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Toffolo?= Date: Fri, 19 Jan 2024 19:41:37 -0800 Subject: [PATCH 62/65] Skip tests that would fail due to unavailable solver features --- test/dbft_test.py | 5 ++- test/examples_test.py | 9 ++--- test/mip_files_test.py | 15 ++++++-- test/mip_test.py | 62 +++++++++++++++++++++++++++--- test/numpy_test.py | 10 ++--- test/rcpsp_test.py | 22 ++++++++--- test/test_conflict.py | 8 ++++ test/test_model.py | 79 ++++++++++++++++++++++++++++++++++++++- test/two_dim_pack_test.py | 17 +++++++-- test/util.py | 23 ++++++++++++ 10 files changed, 217 insertions(+), 33 deletions(-) create mode 100644 test/util.py diff --git a/test/dbft_test.py b/test/dbft_test.py index 52bf488a..4928e514 100644 --- a/test/dbft_test.py +++ b/test/dbft_test.py @@ -4,6 +4,7 @@ from os import environ from itertools import product import pytest +import mip.gurobi import mip.highs from mip import ( Model, @@ -16,11 +17,12 @@ GUROBI, HIGHS, ) +from util import skip_on TOL = 1e-4 SOLVERS = [CBC] -if "GUROBI_HOME" in environ: +if mip.gurobi.has_gurobi and "GUROBI_HOME" in environ: SOLVERS += [GUROBI] if mip.highs.has_highs: SOLVERS += [HIGHS] @@ -477,6 +479,7 @@ def create_model(solver, N, tMax): return m +@skip_on(NotImplementedError) @pytest.mark.parametrize("pdata", PDATA.keys()) @pytest.mark.parametrize("solver", SOLVERS) def test_dbft_mip(solver, pdata): diff --git a/test/examples_test.py b/test/examples_test.py index 7c6a4495..67142883 100644 --- a/test/examples_test.py +++ b/test/examples_test.py @@ -11,23 +11,22 @@ import mip.gurobi import mip.highs from mip import CBC, GUROBI, HIGHS +from util import skip_on EXAMPLES = glob(join("..", "examples", "*.py")) + glob(join(".", "examples", "*.py")) SOLVERS = [CBC] -if mip.gurobi.has_gurobi: +if mip.gurobi.has_gurobi and "GUROBI_HOME" in environ: SOLVERS += [GUROBI] if mip.highs.has_highs: SOLVERS += [HIGHS] +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, example", product(SOLVERS, EXAMPLES)) def test_examples(solver, example): """Executes a given example with using solver 'solver'""" environ["SOLVER_NAME"] = solver loader = importlib.machinery.SourceFileLoader("example", example) mod = types.ModuleType(loader.name) - try: - loader.exec_module(mod) - except NotImplementedError as e: - print("Skipping test for example '{}': {}".format(example, e)) + loader.exec_module(mod) diff --git a/test/mip_files_test.py b/test/mip_files_test.py index a5a34c1d..785688c9 100644 --- a/test/mip_files_test.py +++ b/test/mip_files_test.py @@ -2,12 +2,16 @@ from glob import glob +from itertools import product from os import environ from os.path import basename, dirname, join, exists -from itertools import product + import pytest -import mip -from mip import Model, OptimizationStatus, GUROBI, CBC + +import mip.gurobi +import mip.highs +from mip import Model, OptimizationStatus, CBC, GUROBI, HIGHS +from util import skip_on # for each MIP in test/data, best lower and upper bounds # to be used when checking optimization results @@ -87,8 +91,10 @@ MAX_NODES = 10 SOLVERS = [CBC] -if "GUROBI_HOME" in environ: +if mip.gurobi.has_gurobi and "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] # check availability of test data DATA_DIR = join(join(dirname(mip.__file__)[0:-3], "test"), "data") @@ -107,6 +113,7 @@ INSTS = INSTS + glob(join(DATA_DIR, "*" + exti)) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) def test_mip_file(solver: str, instance: str): """Tests optimization of MIP models stored in .mps or .lp files""" diff --git a/test/mip_test.py b/test/mip_test.py index 56d3bb9a..10ab6cb0 100644 --- a/test/mip_test.py +++ b/test/mip_test.py @@ -1,22 +1,28 @@ """Tests for Python-MIP""" +import math from itertools import product -import pytest +from os import environ + import networkx as nx +import mip.gurobi import mip.highs from mip import Model, xsum, OptimizationStatus, MAXIMIZE, BINARY, INTEGER -from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, HIGHS, Column +from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, HIGHS, Column, Constr from os import environ +from util import skip_on import math +import pytest TOL = 1e-4 SOLVERS = [CBC] -if "GUROBI_HOME" in environ: +if mip.gurobi.has_gurobi and "GUROBI_HOME" in environ: SOLVERS += [GUROBI] if mip.highs.has_highs: SOLVERS += [HIGHS] +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_column_generation(solver: str): L = 250 # bar length @@ -86,6 +92,7 @@ def test_column_generation(solver: str): assert round(master.objective_value) == 3 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_cutting_stock(solver: str): n = 10 # maximum number of bars @@ -122,6 +129,7 @@ def test_cutting_stock(solver: str): assert sum(x.x for x in model.vars) >= 5 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_knapsack(solver: str): p = [10, 13, 18, 31, 7, 15] @@ -153,6 +161,7 @@ def test_knapsack(solver: str): assert abs(m.objective.expr[x[1]] - 28) <= 1e-10 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_queens(solver: str): """MIP model n-queens""" @@ -207,6 +216,7 @@ def test_queens(solver: str): assert rows_with_queens == n # "feasible solution" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_tsp(solver: str): """tsp related tests""" @@ -312,6 +322,7 @@ def generate_constrs(self, model: Model, depth: int = 0, npass: int = 0): model.add_cut(cut) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_tsp_cuts(solver: str): """tsp related tests""" @@ -383,9 +394,8 @@ def test_tsp_cuts(solver: str): assert abs(m.objective_value - 262) <= TOL # "mip model objective" -# Exclude HiGHS solver, which doesn't support MIP start. -SOLVERS_WITH_MIPSTART = [s for s in SOLVERS if s != HIGHS] -@pytest.mark.parametrize("solver", SOLVERS_WITH_MIPSTART) +@skip_on(NotImplementedError) +@pytest.mark.parametrize("solver", SOLVERS) def test_tsp_mipstart(solver: str): """tsp related tests""" N = ["a", "b", "c", "d", "e", "f", "g"] @@ -568,6 +578,7 @@ def test_obj_const2(self, solver: str): assert model.objective_const == 1 +@skip_on(NotImplementedError) @pytest.mark.parametrize("val", range(1, 4)) @pytest.mark.parametrize("solver", SOLVERS) def test_variable_bounds(solver: str, val: int): @@ -583,6 +594,7 @@ def test_variable_bounds(solver: str, val: int): assert round(y.x) == val +@skip_on(NotImplementedError) @pytest.mark.parametrize("val", range(1, 4)) @pytest.mark.parametrize("solver", SOLVERS) def test_linexpr_x(solver: str, val: int): @@ -611,6 +623,7 @@ def test_linexpr_x(solver: str, val: int): assert abs((x + 2 * y + x + 1 + x / 2).x - (x.x + 2 * y.x + x.x + 1 + x.x / 2)) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_add_column(solver: str): """Simple test which add columns in a specific way""" @@ -640,6 +653,7 @@ def test_add_column(solver: str): assert x in example_constr1.expr.expr +@skip_on(NotImplementedError) @pytest.mark.parametrize("val", range(1, 4)) @pytest.mark.parametrize("solver", SOLVERS) def test_float(solver: str, val: int): @@ -658,3 +672,39 @@ def test_float(solver: str, val: int): assert y.x == float(y) # test linear expressions. assert float(x + y) == (x + y).x + + +@skip_on(NotImplementedError) +@pytest.mark.parametrize("solver", SOLVERS) +def test_empty_useless_constraint_is_considered(solver: str): + m = Model("empty_constraint", solver_name=solver) + x = m.add_var(name="x") + y = m.add_var(name="y") + m.add_constr(xsum([]) <= 1, name="c_empty") # useless, empty constraint + m.add_constr(x + y <= 5, name="c1") + m.add_constr(2 * x + y <= 6, name="c2") + m.objective = maximize(x + 2 * y) + m.optimize() + # check objective + assert m.status == OptimizationStatus.OPTIMAL + assert abs(m.objective.x - 10) < TOL + # check that all names of constraints could be queried + assert {c.name for c in m.constrs} == {"c1", "c2", "c_empty"} + assert all(isinstance(m.constr_by_name(c_name), Constr) for c_name in ("c1", "c2", "c_empty")) + + +@skip_on(NotImplementedError) +@pytest.mark.parametrize("solver", SOLVERS) +def test_empty_contradictory_constraint_is_considered(solver: str): + m = Model("empty_constraint", solver_name=solver) + x = m.add_var(name="x") + y = m.add_var(name="y") + m.add_constr(xsum([]) <= -1, name="c_contra") # contradictory empty constraint + m.add_constr(x + y <= 5, name="c1") + m.objective = maximize(x + 2 * y) + m.optimize() + # assert infeasibility of problem + assert m.status in (OptimizationStatus.INF_OR_UNBD, OptimizationStatus.INFEASIBLE) + # check that all names of constraints could be queried + assert {c.name for c in m.constrs} == {"c1", "c_contra"} + assert all(isinstance(m.constr_by_name(c_name), Constr) for c_name in ("c1", "c_contra")) diff --git a/test/numpy_test.py b/test/numpy_test.py index 9a7c4b65..d261c843 100644 --- a/test/numpy_test.py +++ b/test/numpy_test.py @@ -1,12 +1,9 @@ -from itertools import product -import pytest import numpy as np -from mip import Model, xsum, OptimizationStatus, MAXIMIZE, BINARY, INTEGER -from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, Column +from mip import Model, OptimizationStatus from mip.ndarray import LinExprTensor -from os import environ import time -import sys + +from util import skip_on def test_numpy(): @@ -32,6 +29,7 @@ def test_numpy(): assert result == OptimizationStatus.OPTIMAL +@skip_on(NotImplementedError) def test_LinExprTensor(): model = Model() x = model.add_var_tensor(shape=(3,), name="x") diff --git a/test/rcpsp_test.py b/test/rcpsp_test.py index 812e82e4..e127ddba 100644 --- a/test/rcpsp_test.py +++ b/test/rcpsp_test.py @@ -1,22 +1,30 @@ """Set of tests for solving the LP relaxation""" -from glob import glob -from os import environ import json +from glob import glob from itertools import product +from os import environ + import pytest -from mip import CBC, GUROBI, OptimizationStatus + +import mip.gurobi +import mip.highs +from mip import CBC, GUROBI, HIGHS, OptimizationStatus from mip_rcpsp import create_mip +from util import skip_on INSTS = glob("./data/rcpsp*.json") + glob("./test/data/rcpsp*.json") TOL = 1e-4 SOLVERS = [CBC] -if "GUROBI_HOME" in environ: +if mip.gurobi.has_gurobi and "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) def test_rcpsp_relax(solver: str, instance: str): """tests the solution of the LP relaxation of different rcpsp instances""" @@ -37,6 +45,7 @@ def test_rcpsp_relax(solver: str, instance: str): assert abs(z_relax - mip.objective_value) <= TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) def test_rcpsp_relax_mip(solver: str, instance: str): """tests the solution of the LP relaxation of different rcpsp instances""" @@ -58,6 +67,7 @@ def test_rcpsp_relax_mip(solver: str, instance: str): assert abs(z_relax - mip.objective_value) <= TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) def test_rcpsp_mip(solver: str, instance: str): """tests the solution of different RCPSP MIPs""" @@ -97,6 +107,8 @@ def test_rcpsp_mip(solver: str, instance: str): xOn = [v for v in mip.vars if v.x >= 0.99 and v.name.startswith("x(")] assert len(xOn) == len(J) + +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) def test_rcpsp_mipstart(solver: str, instance: str): """tests the solution of different rcpsps MIPs with uwing MIPStarts""" @@ -118,6 +130,6 @@ def test_rcpsp_mipstart(solver: str, instance: str): mip.cut_passes = 0 mip.start = [(mip.var_by_name(n), v) for (n, v) in mipstart] mip.optimize(max_nodes=3) - assert mip.status in [OptimizationStatus.FEASIBLE, + assert mip.status in [OptimizationStatus.FEASIBLE, OptimizationStatus.OPTIMAL] assert abs(mip.objective_value - z_ub) <= TOL diff --git a/test/test_conflict.py b/test/test_conflict.py index 215d3a2d..b48253ca 100644 --- a/test/test_conflict.py +++ b/test/test_conflict.py @@ -5,7 +5,10 @@ import random import numpy as np +from util import skip_on + +@skip_on(NotImplementedError) def test_conflict_finder(): mdl = mip.Model(name="infeasible_model_continuous") var = mdl.add_var(name="x", var_type=mip.CONTINUOUS, lb=-mip.INF, ub=mip.INF) @@ -18,6 +21,7 @@ def test_conflict_finder(): assert set(["lower_bound", "upper_bound"]) == iis_names +@skip_on(NotImplementedError) def test_conflict_finder_iis(): mdl = mip.Model(name="infeasible_model_continuous") var_x = mdl.add_var(name="x", var_type=mip.CONTINUOUS, lb=-mip.INF, ub=mip.INF) @@ -32,6 +36,7 @@ def test_conflict_finder_iis(): assert set(["lower_bound", "upper_bound"]) == iis_names +@skip_on(NotImplementedError) def test_conflict_finder_iis_additive_method(): mdl = mip.Model(name="infeasible_model_continuous") var_x = mdl.add_var(name="x", var_type=mip.CONTINUOUS, lb=-mip.INF, ub=mip.INF) @@ -46,6 +51,7 @@ def test_conflict_finder_iis_additive_method(): assert set(["lower_bound", "upper_bound"]) == iis_names +@skip_on(NotImplementedError) def test_conflict_finder_iis_additive_method_two_options(): mdl = mip.Model(name="infeasible_model_continuous") var_x = mdl.add_var(name="x", var_type=mip.CONTINUOUS, lb=-mip.INF, ub=mip.INF) @@ -62,6 +68,7 @@ def test_conflict_finder_iis_additive_method_two_options(): ) +@skip_on(NotImplementedError) def test_conflict_finder_feasible(): mdl = mip.Model(name="feasible_model") var = mdl.add_var(name="x", var_type=mip.CONTINUOUS, lb=-mip.INF, ub=mip.INF) @@ -98,6 +105,7 @@ def build_infeasible_cont_model( return mdl +@skip_on(NotImplementedError) def test_coflict_relaxer(): # logger config # handler = logging.StreamHandler(sys.stdout) diff --git a/test/test_model.py b/test/test_model.py index e04f32c9..00629a55 100644 --- a/test/test_model.py +++ b/test/test_model.py @@ -1,9 +1,10 @@ -import os import re +from os import environ import pytest import mip +import mip.gurobi import mip.highs from mip import ( CBC, @@ -20,10 +21,11 @@ CONTINUOUS, BINARY, ) +from util import skip_on TOL = 1e-4 SOLVERS = [CBC] -if "GUROBI_HOME" in os.environ: +if mip.gurobi.has_gurobi and "GUROBI_HOME" in environ: SOLVERS += [GUROBI] if mip.highs.has_highs: SOLVERS += [HIGHS] @@ -31,6 +33,7 @@ # Overall Optimization Tests +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("var_type", (CONTINUOUS, INTEGER)) def test_minimize_single_continuous_or_integer_variable_with_default_bounds( @@ -45,6 +48,7 @@ def test_minimize_single_continuous_or_integer_variable_with_default_bounds( assert abs(m.objective_value) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("var_type", (CONTINUOUS, INTEGER)) def test_maximize_single_continuous_or_integer_variable_with_default_bounds( @@ -59,6 +63,7 @@ def test_maximize_single_continuous_or_integer_variable_with_default_bounds( assert m.objective_value is None +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize( "sense,status,xvalue,objvalue", @@ -79,6 +84,7 @@ def test_single_binary_variable_with_default_bounds( assert abs(m.objective_value - objvalue) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("var_type", (CONTINUOUS, INTEGER)) @pytest.mark.parametrize( @@ -112,6 +118,7 @@ def test_single_continuous_or_integer_variable_with_different_bounds( assert abs(m.objective_value - max_obj) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize( "lb,ub,min_obj,max_obj", @@ -139,6 +146,7 @@ def test_binary_variable_with_different_bounds(solver, lb, ub, min_obj, max_obj) assert abs(m.objective_value - max_obj) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_binary_variable_illegal_bounds(solver): m = Model(solver_name=solver) @@ -150,6 +158,7 @@ def test_binary_variable_illegal_bounds(solver): m.add_var("x", ub=2, var_type=BINARY) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("sense", (MINIMIZE, MAXIMIZE)) @pytest.mark.parametrize( @@ -168,6 +177,7 @@ def test_contradictory_variable_bounds(solver, sense: str, var_type: str, lb, ub assert m.status == OptimizationStatus.INFEASIBLE +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_float_bounds_for_integer_variable(solver): # Minimum Case @@ -187,6 +197,7 @@ def test_float_bounds_for_integer_variable(solver): assert abs(m.objective_value - 3) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("sense", (MINIMIZE, MAXIMIZE)) def test_single_default_variable_with_nothing_to_do(solver, sense): @@ -198,6 +209,7 @@ def test_single_default_variable_with_nothing_to_do(solver, sense): assert abs(m.objective_value) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("var_type", (CONTINUOUS, INTEGER, BINARY)) @pytest.mark.parametrize("obj", (1.2, 2)) @@ -222,6 +234,7 @@ def test_single_variable_with_different_non_zero_objectives(solver, var_type, ob # Variable Tests +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_hashes_of_variables(solver): m = Model(solver_name=solver) @@ -232,6 +245,7 @@ def test_hashes_of_variables(solver): assert hash(y) == 1 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("constant", (-1, 1.2, 2)) def test_addition_of_var_with_non_zero_constant(solver, constant): @@ -256,6 +270,7 @@ def test_addition_of_var_with_non_zero_constant(solver, constant): assert y.expr == {x: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_addition_of_var_with_zero(solver): m = Model(solver_name=solver) @@ -276,6 +291,7 @@ def test_addition_of_var_with_zero(solver): assert hash(y) == hash(x) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_addition_of_two_vars(solver): m = Model(solver_name=solver) @@ -295,6 +311,7 @@ def test_addition_of_two_vars(solver): assert z.expr == {x: 1, y: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_addition_of_var_with_linear_expression(solver): m = Model(solver_name=solver) @@ -317,6 +334,7 @@ def test_addition_of_var_with_linear_expression(solver): assert w.expr == {x: 1, y: 1, z: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_addition_of_var_with_illegal_type(solver): m = Model(solver_name=solver) @@ -329,6 +347,7 @@ def test_addition_of_var_with_illegal_type(solver): "1" + x +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("constant", (-1, 1.2, 2)) def test_subtraction_of_var_with_non_zero_constant(solver, constant): @@ -353,6 +372,7 @@ def test_subtraction_of_var_with_non_zero_constant(solver, constant): assert y.expr == {x: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_subtraction_of_var_with_zero(solver): m = Model(solver_name=solver) @@ -374,6 +394,7 @@ def test_subtraction_of_var_with_zero(solver): assert hash(y) == hash(x) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_subtraction_of_two_vars(solver): m = Model(solver_name=solver) @@ -393,6 +414,7 @@ def test_subtraction_of_two_vars(solver): assert z.expr == {x: 1, y: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_subtraction_of_var_with_linear_expression(solver): m = Model(solver_name=solver) @@ -415,6 +437,7 @@ def test_subtraction_of_var_with_linear_expression(solver): assert w.expr == {x: 1, y: -1, z: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_subtraction_of_var_with_illegal_type(solver): m = Model(solver_name=solver) @@ -427,6 +450,7 @@ def test_subtraction_of_var_with_illegal_type(solver): "1" - x +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-1, 0, 1.2, 2)) def test_multiply_var_with_coefficient(solver, coefficient): @@ -451,6 +475,7 @@ def test_multiply_var_with_coefficient(solver, coefficient): assert y.expr == {x: coefficient} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_multiply_var_with_illegal_coefficient(solver): m = Model(solver_name=solver) @@ -462,6 +487,7 @@ def test_multiply_var_with_illegal_coefficient(solver): x * "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-1, 1.2, 2)) def test_divide_var_with_non_zero_coefficient(solver, coefficient): @@ -481,6 +507,7 @@ def test_divide_var_with_non_zero_coefficient(solver, coefficient): assert y.expr == {x: 1/coefficient} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_divide_var_with_illegal_coefficient(solver): m = Model(solver_name=solver) @@ -490,6 +517,7 @@ def test_divide_var_with_illegal_coefficient(solver): y = x / "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_divide_var_with_zero(solver): m = Model(solver_name=solver) @@ -499,6 +527,7 @@ def test_divide_var_with_zero(solver): x / 0 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_negate_variable(solver): m = Model(solver_name=solver) @@ -510,6 +539,7 @@ def test_negate_variable(solver): assert y.expr == {x: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("constant", (-1, 0, 1)) def test_constraint_with_var_and_const(solver, constant): @@ -556,6 +586,7 @@ def test_constraint_with_var_and_const(solver, constant): assert constr.sense == "<" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_constraint_with_var_and_var(solver): m = Model(solver_name=solver) @@ -581,6 +612,7 @@ def test_constraint_with_var_and_var(solver): assert constr.sense == ">" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_constraint_with_var_and_lin_expr(solver): m = Model(solver_name=solver) @@ -609,6 +641,7 @@ def test_constraint_with_var_and_lin_expr(solver): assert constr.sense == ">" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_constraint_with_var_and_illegal_type(solver): m = Model(solver_name=solver) @@ -633,6 +666,7 @@ def test_constraint_with_var_and_illegal_type(solver): "1" >= x +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_query_variable_attributes(solver): m = Model(solver_name=solver, sense=MAXIMIZE) @@ -666,6 +700,7 @@ def test_query_variable_attributes(solver): # TODO check Xn in case of additional (sub-optimal) solutions +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_setting_variable_attributes(solver): m = Model(solver_name=solver, sense=MAXIMIZE) @@ -717,6 +752,7 @@ def test_setting_variable_attributes(solver): assert abs(y.x - 5) < TOL +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_forbidden_overwrites_of_variable_attributes(solver): m = Model(solver_name=solver) @@ -739,6 +775,7 @@ def test_forbidden_overwrites_of_variable_attributes(solver): x.rc = 6 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_wrong_overwrites_of_variable_attributes(solver): m = Model(solver_name=solver) @@ -767,6 +804,7 @@ def test_wrong_overwrites_of_variable_attributes(solver): # LinExpr Tests +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("constant", (-1, 0, 1.5, 2)) def test_addition_of_lin_expr_with_constant(solver, constant): @@ -786,6 +824,7 @@ def test_addition_of_lin_expr_with_constant(solver, constant): assert term_left.expr == {x: 1, y: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_addition_of_lin_expr_with_var(solver): m = Model(solver_name=solver) @@ -800,6 +839,7 @@ def test_addition_of_lin_expr_with_var(solver): assert term_right.expr == {x: 1, y: 1, z: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_addition_of_lin_expr_with_lin_expr(solver): m = Model(solver_name=solver) @@ -817,6 +857,7 @@ def test_addition_of_lin_expr_with_lin_expr(solver): assert added_term.expr == {x: 1, y: 1, a: 1, b: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_addition_of_lin_expr_with_illegal_type(solver): m = Model(solver_name=solver) @@ -831,6 +872,7 @@ def test_addition_of_lin_expr_with_illegal_type(solver): "1" + term +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("constant", (-1, 0, 1.5, 2)) def test_inplace_addition_of_lin_expr_with_constant(solver, constant): @@ -846,6 +888,7 @@ def test_inplace_addition_of_lin_expr_with_constant(solver, constant): assert term.expr == {x: 1, y: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_addition_of_lin_expr_with_var(solver): m = Model(solver_name=solver) @@ -860,6 +903,7 @@ def test_inplace_addition_of_lin_expr_with_var(solver): assert term.expr == {x: 1, y: 1, z: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_addition_of_lin_expr_with_lin_expr(solver): m = Model(solver_name=solver) @@ -877,6 +921,7 @@ def test_inplace_addition_of_lin_expr_with_lin_expr(solver): assert term.expr == {x: 1, y: 1, a: 1, b: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_addition_of_lin_expr_with_illegal_type(solver): m = Model(solver_name=solver) @@ -888,6 +933,7 @@ def test_inplace_addition_of_lin_expr_with_illegal_type(solver): term += "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("constant", (-1, 0, 1.5, 2)) def test_subtraction_of_lin_expr_and_constant(solver, constant): @@ -907,6 +953,7 @@ def test_subtraction_of_lin_expr_and_constant(solver, constant): assert term_left.expr == {x: -1, y: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_subtraction_of_lin_expr_and_var(solver): m = Model(solver_name=solver) @@ -921,6 +968,7 @@ def test_subtraction_of_lin_expr_and_var(solver): assert term_right.expr == {x: 1, y: 1, z: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_subtraction_of_lin_expr_with_lin_expr(solver): m = Model(solver_name=solver) @@ -938,6 +986,7 @@ def test_subtraction_of_lin_expr_with_lin_expr(solver): assert sub_term.expr == {x: 1, y: 1, a: -1, b: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_subtraction_of_lin_expr_and_illegal_type(solver): m = Model(solver_name=solver) @@ -952,6 +1001,7 @@ def test_subtraction_of_lin_expr_and_illegal_type(solver): "1" - term +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("constant", (-1, 0, 1.5, 2)) def test_inplace_subtraction_of_lin_expr_and_constant(solver, constant): @@ -967,6 +1017,7 @@ def test_inplace_subtraction_of_lin_expr_and_constant(solver, constant): assert term.expr == {x: 1, y: 1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_subtraction_of_lin_expr_and_var(solver): m = Model(solver_name=solver) @@ -981,6 +1032,7 @@ def test_inplace_subtraction_of_lin_expr_and_var(solver): assert term.expr == {x: 1, y: 1, z: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_subtraction_of_lin_expr_and_lin_expr(solver): m = Model(solver_name=solver) @@ -998,6 +1050,7 @@ def test_inplace_subtraction_of_lin_expr_and_lin_expr(solver): assert term.expr == {x: 1, y: 1, a: -1, b: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_subtraction_of_lin_expr_and_illegal_type(solver): m = Model(solver_name=solver) @@ -1009,6 +1062,7 @@ def test_inplace_subtraction_of_lin_expr_and_illegal_type(solver): term -= "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-1, 0, 1.2, 2)) def test_multiply_lin_expr_with_coefficient(solver, coefficient): @@ -1028,6 +1082,7 @@ def test_multiply_lin_expr_with_coefficient(solver, coefficient): assert left.expr == {x: coefficient, y: coefficient} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_multiply_lin_expr_with_illegal_coefficient(solver): m = Model(solver_name=solver) @@ -1042,6 +1097,7 @@ def test_multiply_lin_expr_with_illegal_coefficient(solver): term * "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-1, 0, 1.2, 2)) def test_inplace_multiplication_lin_expr_with_coefficient(solver, coefficient): @@ -1056,6 +1112,7 @@ def test_inplace_multiplication_lin_expr_with_coefficient(solver, coefficient): assert term.expr == {x: coefficient, y: coefficient} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_multiplication_lin_expr_with_illegal_coefficient(solver): m = Model(solver_name=solver) @@ -1067,6 +1124,7 @@ def test_inplace_multiplication_lin_expr_with_illegal_coefficient(solver): term *= "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-1, 1.2, 2)) def test_division_lin_expr_non_zero_coefficient(solver, coefficient): @@ -1081,6 +1139,7 @@ def test_division_lin_expr_non_zero_coefficient(solver, coefficient): assert right.expr == {x: 1 / coefficient, y: 1 / coefficient} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_divide_lin_expr_with_illegal_coefficient(solver): m = Model(solver_name=solver) @@ -1092,6 +1151,7 @@ def test_divide_lin_expr_with_illegal_coefficient(solver): term / "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_divide_lin_expr_by_zero(solver): m = Model(solver_name=solver) @@ -1103,6 +1163,7 @@ def test_divide_lin_expr_by_zero(solver): term / 0 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-1, 1.2, 2)) def test_inplace_division_lin_expr_non_zero_coefficient(solver, coefficient): @@ -1117,6 +1178,7 @@ def test_inplace_division_lin_expr_non_zero_coefficient(solver, coefficient): assert term.expr == {x: 1 / coefficient, y: 1 / coefficient} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_division_lin_expr_by_zero(solver): m = Model(solver_name=solver) @@ -1128,6 +1190,7 @@ def test_inplace_division_lin_expr_by_zero(solver): term /= 0 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_inplace_division_lin_expr_with_illegal_coefficient(solver): m = Model(solver_name=solver) @@ -1139,6 +1202,7 @@ def test_inplace_division_lin_expr_with_illegal_coefficient(solver): term /= "1" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-1, 1.2, 2)) def test_negating_lin_expr_non_zero_coefficient(solver, coefficient): @@ -1153,6 +1217,7 @@ def test_negating_lin_expr_non_zero_coefficient(solver, coefficient): assert neg.expr == {x: -1, y: -1} +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_len_of_lin_expr(solver): m = Model(solver_name=solver) @@ -1164,6 +1229,7 @@ def test_len_of_lin_expr(solver): assert len(x + y + 1) == 2 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) @pytest.mark.parametrize("coefficient", (-2, 0, 1.1, 3)) def test_add_term_with_valid_input(solver, coefficient): @@ -1201,6 +1267,7 @@ def test_add_term_with_valid_input(solver, coefficient): term.add_term(add_term, coefficient) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_hash(solver): m = Model(solver_name=solver) @@ -1211,6 +1278,7 @@ def test_hash(solver): assert type(hash(term)) == int +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_copy(solver): m = Model(solver_name=solver) @@ -1226,6 +1294,7 @@ def test_copy(solver): assert id(term.expr) != id(term_copy.expr) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_constraint_with_lin_expr_and_lin_expr(solver): m = Model(solver_name=solver) @@ -1256,6 +1325,7 @@ def test_constraint_with_lin_expr_and_lin_expr(solver): assert constr.sense == ">" +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_query_attributes_of_lin_expr(solver): m = Model(solver_name=solver, sense=MAXIMIZE) @@ -1280,6 +1350,7 @@ def test_query_attributes_of_lin_expr(solver): m.optimize() +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_objective(solver): m = Model(solver_name=solver, sense=MAXIMIZE) @@ -1317,6 +1388,7 @@ def test_objective(solver): assert m.objective_value == 1.5 assert m.objective_value == m.objective.x +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_remove(solver): m = Model(solver_name=solver) @@ -1333,6 +1405,7 @@ def test_remove(solver): m.remove(constr) m.remove(x) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_add_equation(solver): m = Model(solver_name=solver) @@ -1344,6 +1417,7 @@ def test_add_equation(solver): assert x.x == pytest.approx(23.5) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_change_objective_sense(solver): m = Model(solver_name=solver) @@ -1361,6 +1435,7 @@ def test_change_objective_sense(solver): assert status == OptimizationStatus.OPTIMAL assert x.x == pytest.approx(10.0) +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver", SOLVERS) def test_solve_relaxation(solver): m = Model(solver_name=solver) diff --git a/test/two_dim_pack_test.py b/test/two_dim_pack_test.py index ce3f3469..e8386df9 100644 --- a/test/two_dim_pack_test.py +++ b/test/two_dim_pack_test.py @@ -1,12 +1,17 @@ """Set of tests for solving the LP relaxation""" -from glob import glob -from os import environ import json +from glob import glob from itertools import product +from os import environ + import pytest -from mip import CBC, GUROBI, OptimizationStatus + +import mip.gurobi +import mip.highs +from mip import CBC, GUROBI, HIGHS, OptimizationStatus from mip_2d_pack import create_mip +from util import skip_on INSTS = glob("./data/two_dim_pack_p*.json") + glob( "./test/data/two_dim_pack_*.json" @@ -15,10 +20,13 @@ TOL = 1e-4 SOLVERS = [CBC] -if "GUROBI_HOME" in environ: +if mip.gurobi.has_gurobi and "GUROBI_HOME" in environ: SOLVERS += [GUROBI] +if mip.highs.has_highs: + SOLVERS += [HIGHS] +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) def test_2dpack_relax_and_cut(solver: str, instance: str): """tests the solution of the LP relaxation of different 2D pack instances""" @@ -48,6 +56,7 @@ def test_2dpack_relax_and_cut(solver: str, instance: str): assert sobj <= best + 1e-5 +@skip_on(NotImplementedError) @pytest.mark.parametrize("solver, instance", product(SOLVERS, INSTS)) def test_2dpack_mip(solver: str, instance: str): """tests the MIP solution of different 2D pack instances""" diff --git a/test/util.py b/test/util.py new file mode 100644 index 00000000..a7926763 --- /dev/null +++ b/test/util.py @@ -0,0 +1,23 @@ +import pytest + +from functools import wraps + + +def skip_on(exception): + """ + Skips the test in case the given exception is raised. + :param exception: exception to consider + :return: decorator function + """ + + def decorator_func(f): + @wraps(f) + def wrapper(*args, **kwargs): + try: + return f(*args, **kwargs) + except exception as e: + pytest.skip(str(e)) + + return wrapper + + return decorator_func From 8799edece778a1370b89506329fdc898e42a0ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Toffolo?= Date: Fri, 19 Jan 2024 20:07:25 -0800 Subject: [PATCH 63/65] Skip other unnecessary HiGHS tests --- examples/gen_cuts_mip.py | 16 +++++++--------- mip/highs.py | 6 ++++++ test/numpy_test.py | 8 +++++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/examples/gen_cuts_mip.py b/examples/gen_cuts_mip.py index f71edaec..71966db9 100644 --- a/examples/gen_cuts_mip.py +++ b/examples/gen_cuts_mip.py @@ -1,23 +1,21 @@ """This example reads a MIP (in .lp or .mps), solves its linear programming relaxation and then tests the impact of adding different types of cutting -planes. In the end, the it informs which cut generator produced the best bound -improvement.""" +planes. In the end, it informs which cut generator produced the best bound +improvement. +""" from textwrap import shorten -import sys from mip import Model, CutType, OptimizationStatus import mip -lp_path = "" - # using test data lp_path = mip.__file__.replace("mip/__init__.py", "test/data/1443_0-9.lp").replace( "mip\\__init__.py", "test\\data\\1443_0-9.lp" ) m = Model() -if m.solver_name.upper() in ["GRB", "GUROBI"]: - print("This feature is currently not supported in Gurobi.") +if m.solver_name.upper() != mip.CBC: + print("This feature is currently supported only in CBC.") else: m.read(lp_path) @@ -55,8 +53,8 @@ best_cut = ct print( - "Linear programming relaxation bound now: %g, improvement of %.2f" - % (m2.objective_value, perc_impr) + f"Linear programming relaxation bound now: " + f"{m2.objective_value:.2f}, improvement of {perc_impr:.2f}" ) else: continue diff --git a/mip/highs.py b/mip/highs.py index 08076ea0..4534dd04 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -448,6 +448,12 @@ def optimize( check(self._lib.Highs_run(self._model)) + # check whether unsupported callbacks were set + if self.model.lazy_constrs_generator: + raise NotImplementedError("HiGHS doesn't support lazy constraints at the moment") + if self.model.cuts_generator: + raise NotImplementedError("HiGHS doesn't support cuts generator at the moment") + # store solution values for later access opt_status = self.get_status() if opt_status in ( diff --git a/test/numpy_test.py b/test/numpy_test.py index d261c843..570fa747 100644 --- a/test/numpy_test.py +++ b/test/numpy_test.py @@ -1,14 +1,16 @@ +import time + import numpy as np + from mip import Model, OptimizationStatus from mip.ndarray import LinExprTensor -import time - from util import skip_on +@skip_on(NotImplementedError) def test_numpy(): model = Model() - N = 1000 + N = 100 start = time.time() x = model.add_var_tensor(shape=(N, N), name="x") From 6c754521b44670275d861207c5cfd52d59c341eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Toffolo?= Date: Sat, 20 Jan 2024 20:07:50 -0800 Subject: [PATCH 64/65] Skip HiGHS tests for Python 3.12 and Windows (temporarily) --- .github/workflows/github-ci.yml | 48 ++++++++++----------------------- pyproject.toml | 25 ++++++++++++----- 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/.github/workflows/github-ci.yml b/.github/workflows/github-ci.yml index a94ad94c..3f9e5eeb 100644 --- a/.github/workflows/github-ci.yml +++ b/.github/workflows/github-ci.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.7.14 + python-version: 3.11 - name: Upgrade pip run: python -m pip install --upgrade pip @@ -39,20 +39,14 @@ jobs: strategy: fail-fast: false matrix: - # temporarily downgraded to 3.7.9 and 3.8.10 due to a bug https://github.com/actions/setup-python/issues/402 - python-version: ["3.7.9", "3.8.10", "3.9.13", "3.10.9", "3.11.1", "pypy3.9-v7.3.9"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3.9-v7.3.15"] os: [macos-11, macos-12, ubuntu-20.04, ubuntu-22.04, windows-2019, windows-2022] exclude: # temporarily exclude pypy3 on mac-os as there failing tests caused by bug on cbc side - os: macos-11 - python-version: "pypy3.9-v7.3.9" + python-version: "pypy3.9-v7.3.15" - os: macos-12 - python-version: "pypy3.9-v7.3.9" - # several version (3.7.9 and 3.8.10) at not available at ubuntu-22.04 - - os: ubuntu-22.04 - python-version: "3.7.9" - - os: ubuntu-22.04 - python-version: "3.8.10" + python-version: "pypy3.9-v7.3.15" steps: @@ -63,6 +57,7 @@ jobs: with: python-version: ${{ matrix.python-version }} architecture: x64 + cache: 'pip' - name: Check python version run: python -c "import sys; import platform; print('Python %s implementation %s on %s' % (sys.version, platform.python_implementation(), sys.platform))" @@ -70,34 +65,19 @@ jobs: - name: Upgrade pip run: python -m pip install --upgrade pip - - name: Get pip cache dir - id: pip-cache - run: | - echo "::set-output name=dir::$(pip cache dir)" + - name: Install test and numpy + run: python -m pip install .[test,numpy] - - name: pip cache - uses: actions/cache@v3 - with: - path: ${{ steps.pip-cache.outputs.dir }} - key: ${{ runner.os }}-${{ matrix.python-version }}-pythonpip - - - name: Install mip for testing (PyPy) - if: ${{ matrix.python-version == 'pypy3.9-v7.3.9' }} - run: python -m pip install .[test,numpy,highs] + - name: Install gurobi + if: ${{ matrix.python-version != 'pypy3.9-v7.3.15' }} + run: python -m pip install .[gurobi] - - name: Install mip for testing (CPython) - if: ${{ matrix.python-version != 'pypy3.9-v7.3.9' }} - run: python -m pip install .[test,numpy,gurobi,highs] + - name: Install highs + if: ${{ !contains(matrix.os, 'windows') && !(matrix.os == 'ubuntu-22.04' && matrix.python-version == '3.9') }} + run: python -m pip install .[highs] - name: list installed packages run: python -m pip list - - name: Run tests PyPy - if: ${{ matrix.python-version == 'pypy3.9-v7.3.9'}} - run: | - python -m pytest test --verbose --color=yes --doctest-modules --ignore="test/test_gurobi.py" - - name: Run tests - if: ${{ matrix.python-version != 'pypy3.9-v7.3.9'}} - run: | - python -m pytest test --verbose --color=yes --doctest-modules -Werror + run: python -m pytest test --verbose --color=yes --doctest-modules -Werror diff --git a/pyproject.toml b/pyproject.toml index 00f7a3aa..df8188cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,11 +6,11 @@ build-backend = "setuptools.build_meta" name = "mip" description = "Python tools for Modeling and Solving Mixed-Integer Linear Programs (MIPs)" readme = "README.md" -requires-python = ">=3.7,<3.12" +requires-python = ">=3.7,<3.13" license = {file = "LICENSE"} authors = [ - {name="T.A.M. Toffolo", email="haroldo.santos@gmail.com"}, - {name="H.G. Santos", email="tulio@toffolo.com.br"} + {name="Tulio A.M. Toffolo", email="tulio@toffolo.com.br"}, + {name="Haroldo G. Santos", email="haroldo.santos@gmail.com"} ] maintainers = [ {name="S. Heger", email="heger@m2p.net"} @@ -32,13 +32,24 @@ classifiers = [ ] dynamic = ["version"] -dependencies = ["cffi==1.15.*"] +dependencies = ["cffi>=1.15"] [project.optional-dependencies] -numpy = ["numpy==1.24.*; python_version >= '3.8'", "numpy==1.21.6; python_version == '3.7'"] +numpy = [ + "numpy>=1.25; python_version>='3.9'", + "numpy==1.24.*; python_version=='3.8'", + "numpy==1.21.*; python_version=='3.7'" +] gurobi = ["gurobipy>=8"] -highs = ["highspy>=1.5.3"] -test = ["pytest==7.2.0", "networkx==2.8.8; python_version >= '3.8'", "networkx==2.6.3; python_version == '3.7'", "matplotlib==3.6.2; python_version >= '3.8'", "matplotlib==3.5.3; python_version == '3.7'"] +highs = ["highspy>=1.5.3; python_version<='3.11'"] +test = [ + "pytest>=7.4", + "networkx==2.8.8; python_version>='3.8'", + "networkx==2.6.3; python_version=='3.7'", + "matplotlib>=3.7; python_version>='3.9'", + "matplotlib==3.6.2; python_version=='3.8'", + "matplotlib==3.5.3; python_version=='3.7'" +] [project.urls] "Homepage" = "https://www.python-mip.com" From 6bce7687d2da231341b74f557d32d9d2f2382613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BAlio=20Toffolo?= Date: Sat, 20 Jan 2024 19:42:24 -0800 Subject: [PATCH 65/65] Reduce HiGHS memory footprint in python-mip, fix inconsistencies when reading the model from a file and format with black --- mip/highs.py | 867 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 697 insertions(+), 170 deletions(-) diff --git a/mip/highs.py b/mip/highs.py index 4534dd04..782f7aed 100644 --- a/mip/highs.py +++ b/mip/highs.py @@ -5,7 +5,7 @@ import logging import os.path import sys -from typing import Dict, List, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union import cffi @@ -48,137 +48,640 @@ logger.error(f"An error occurred while loading the HiGHS library:\n{e}") has_highs = False -HEADER = """ -typedef int HighsInt; - -const HighsInt kHighsStatusError = -1; -const HighsInt kHighsStatusOk = 0; -const HighsInt kHighsStatusWarning = 1; - -const HighsInt kHighsObjSenseMinimize = 1; -const HighsInt kHighsObjSenseMaximize = -1; - -const HighsInt kHighsVarTypeContinuous = 0; -const HighsInt kHighsVarTypeInteger = 1; - -const HighsInt kHighsSolutionStatusNone = 0; -const HighsInt kHighsSolutionStatusInfeasible = 1; -const HighsInt kHighsSolutionStatusFeasible = 2; - -const HighsInt kHighsModelStatusNotset = 0; -const HighsInt kHighsModelStatusLoadError = 1; -const HighsInt kHighsModelStatusModelError = 2; -const HighsInt kHighsModelStatusPresolveError = 3; -const HighsInt kHighsModelStatusSolveError = 4; -const HighsInt kHighsModelStatusPostsolveError = 5; -const HighsInt kHighsModelStatusModelEmpty = 6; -const HighsInt kHighsModelStatusOptimal = 7; -const HighsInt kHighsModelStatusInfeasible = 8; -const HighsInt kHighsModelStatusUnboundedOrInfeasible = 9; -const HighsInt kHighsModelStatusUnbounded = 10; -const HighsInt kHighsModelStatusObjectiveBound = 11; -const HighsInt kHighsModelStatusObjectiveTarget = 12; -const HighsInt kHighsModelStatusTimeLimit = 13; -const HighsInt kHighsModelStatusIterationLimit = 14; -const HighsInt kHighsModelStatusUnknown = 15; -const HighsInt kHighsModelStatusSolutionLimit = 16; - -void* Highs_create(void); -void Highs_destroy(void* highs); -HighsInt Highs_readModel(void* highs, const char* filename); -HighsInt Highs_writeModel(void* highs, const char* filename); -HighsInt Highs_run(void* highs); -HighsInt Highs_getModelStatus(const void* highs); -double Highs_getObjectiveValue(const void* highs); -HighsInt Highs_addVar(void* highs, const double lower, const double upper); -HighsInt Highs_addRow( - void* highs, const double lower, const double upper, const HighsInt num_new_nz, - const HighsInt* index, const double* value -); -HighsInt Highs_changeObjectiveOffset(void* highs, const double offset); -HighsInt Highs_changeObjectiveSense(void* highs, const HighsInt sense); -HighsInt Highs_changeColIntegrality( - void* highs, const HighsInt col, const HighsInt integrality -); -HighsInt Highs_changeColsIntegralityByRange( - void* highs, const HighsInt from_col, const HighsInt to_col, - const HighsInt* integrality -); -HighsInt Highs_changeColCost(void* highs, const HighsInt col, const double cost); -HighsInt Highs_changeColBounds( - void* highs, const HighsInt col, const double lower, const double upper -); -HighsInt Highs_changeCoeff( - void* highs, const HighsInt row, const HighsInt col, const double value -); -HighsInt Highs_changeRowBounds( - void* highs, const HighsInt row, const double lower, const double upper -); -HighsInt Highs_getRowsByRange( - const void* highs, const HighsInt from_row, const HighsInt to_row, - HighsInt* num_row, double* lower, double* upper, HighsInt* num_nz, - HighsInt* matrix_start, HighsInt* matrix_index, double* matrix_value -); -HighsInt Highs_getColsByRange( - const void* highs, const HighsInt from_col, const HighsInt to_col, - HighsInt* num_col, double* costs, double* lower, double* upper, - HighsInt* num_nz, HighsInt* matrix_start, HighsInt* matrix_index, - double* matrix_value -); -HighsInt Highs_getObjectiveOffset(const void* highs, double* offset); -HighsInt Highs_getObjectiveSense(const void* highs, HighsInt* sense); -HighsInt Highs_getNumCol(const void* highs); -HighsInt Highs_getNumRow(const void* highs); -HighsInt Highs_getNumNz(const void* highs); -HighsInt Highs_getDoubleInfoValue( - const void* highs, const char* info, double* value -); -HighsInt Highs_getIntInfoValue( - const void* highs, const char* info, int* value -); -HighsInt Highs_getIntOptionValue( - const void* highs, const char* option, HighsInt* value -); -HighsInt Highs_getDoubleOptionValue( - const void* highs, const char* option, double* value -); -HighsInt Highs_getBoolOptionValue( - const void* highs, const char* option, bool* value -); -HighsInt Highs_setIntOptionValue( - void* highs, const char* option, const HighsInt value -); -HighsInt Highs_setDoubleOptionValue( - void* highs, const char* option, const double value -); -HighsInt Highs_setBoolOptionValue( - void* highs, const char* option, const bool value -); -HighsInt Highs_getSolution( - const void* highs, double* col_value, double* col_dual, - double* row_value, double* row_dual -); -HighsInt Highs_setSolution( - const void* highs, double* col_value, double* row_value, - double* col_dual, double* row_dual -); -HighsInt Highs_deleteRowsBySet( - void* highs, const HighsInt num_set_entries, const HighsInt* set -); -HighsInt Highs_deleteColsBySet( - void* highs, const HighsInt num_set_entries, const HighsInt* set -); -""" - if has_highs: - ffi.cdef(HEADER) + ffi.cdef( + """ + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + /* */ + /* This file is part of the HiGHS linear optimization suite */ + /* */ + /* Written and engineered 2008-2024 by Julian Hall, Ivet Galabova, */ + /* Leona Gottwald and Michael Feldmeier */ + /* */ + /* Available as open-source under the MIT License */ + /* */ + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + typedef int HighsInt; + + typedef struct { + int log_type; // cast of HighsLogType + double running_time; + HighsInt simplex_iteration_count; + HighsInt ipm_iteration_count; + double objective_function_value; + int64_t mip_node_count; + double mip_primal_bound; + double mip_dual_bound; + double mip_gap; + double* mip_solution; + } HighsCallbackDataOut; + + typedef struct { + int user_interrupt; + } HighsCallbackDataIn; + + typedef void (*HighsCCallbackType)(int, const char*, + const HighsCallbackDataOut*, + HighsCallbackDataIn*, void*); + + const HighsInt kHighsMaximumStringLength = 512; + + const HighsInt kHighsStatusError = -1; + const HighsInt kHighsStatusOk = 0; + const HighsInt kHighsStatusWarning = 1; + + const HighsInt kHighsVarTypeContinuous = 0; + const HighsInt kHighsVarTypeInteger = 1; + const HighsInt kHighsVarTypeSemiContinuous = 2; + const HighsInt kHighsVarTypeSemiInteger = 3; + const HighsInt kHighsVarTypeImplicitInteger = 4; + + const HighsInt kHighsOptionTypeBool = 0; + const HighsInt kHighsOptionTypeInt = 1; + const HighsInt kHighsOptionTypeDouble = 2; + const HighsInt kHighsOptionTypeString = 3; + + const HighsInt kHighsInfoTypeInt64 = -1; + const HighsInt kHighsInfoTypeInt = 1; + const HighsInt kHighsInfoTypeDouble = 2; + + const HighsInt kHighsObjSenseMinimize = 1; + const HighsInt kHighsObjSenseMaximize = -1; + + const HighsInt kHighsMatrixFormatColwise = 1; + const HighsInt kHighsMatrixFormatRowwise = 2; + + const HighsInt kHighsHessianFormatTriangular = 1; + const HighsInt kHighsHessianFormatSquare = 2; + + const HighsInt kHighsSolutionStatusNone = 0; + const HighsInt kHighsSolutionStatusInfeasible = 1; + const HighsInt kHighsSolutionStatusFeasible = 2; + + const HighsInt kHighsBasisValidityInvalid = 0; + const HighsInt kHighsBasisValidityValid = 1; + + const HighsInt kHighsPresolveStatusNotPresolved = -1; + const HighsInt kHighsPresolveStatusNotReduced = 0; + const HighsInt kHighsPresolveStatusInfeasible = 1; + const HighsInt kHighsPresolveStatusUnboundedOrInfeasible = 2; + const HighsInt kHighsPresolveStatusReduced = 3; + const HighsInt kHighsPresolveStatusReducedToEmpty = 4; + const HighsInt kHighsPresolveStatusTimeout = 5; + const HighsInt kHighsPresolveStatusNullError = 6; + const HighsInt kHighsPresolveStatusOptionsError = 7; + + const HighsInt kHighsModelStatusNotset = 0; + const HighsInt kHighsModelStatusLoadError = 1; + const HighsInt kHighsModelStatusModelError = 2; + const HighsInt kHighsModelStatusPresolveError = 3; + const HighsInt kHighsModelStatusSolveError = 4; + const HighsInt kHighsModelStatusPostsolveError = 5; + const HighsInt kHighsModelStatusModelEmpty = 6; + const HighsInt kHighsModelStatusOptimal = 7; + const HighsInt kHighsModelStatusInfeasible = 8; + const HighsInt kHighsModelStatusUnboundedOrInfeasible = 9; + const HighsInt kHighsModelStatusUnbounded = 10; + const HighsInt kHighsModelStatusObjectiveBound = 11; + const HighsInt kHighsModelStatusObjectiveTarget = 12; + const HighsInt kHighsModelStatusTimeLimit = 13; + const HighsInt kHighsModelStatusIterationLimit = 14; + const HighsInt kHighsModelStatusUnknown = 15; + const HighsInt kHighsModelStatusSolutionLimit = 16; + const HighsInt kHighsModelStatusInterrupt = 17; + + const HighsInt kHighsBasisStatusLower = 0; + const HighsInt kHighsBasisStatusBasic = 1; + const HighsInt kHighsBasisStatusUpper = 2; + const HighsInt kHighsBasisStatusZero = 3; + const HighsInt kHighsBasisStatusNonbasic = 4; + + const HighsInt kHighsCallbackLogging = 0; + const HighsInt kHighsCallbackSimplexInterrupt = 1; + const HighsInt kHighsCallbackIpmInterrupt = 2; + const HighsInt kHighsCallbackMipSolution = 3; + const HighsInt kHighsCallbackMipImprovingSolution = 4; + const HighsInt kHighsCallbackMipLogging = 5; + const HighsInt kHighsCallbackMipInterrupt = 6; + + HighsInt Highs_lpCall(const HighsInt num_col, const HighsInt num_row, + const HighsInt num_nz, const HighsInt a_format, + const HighsInt sense, const double offset, + const double* col_cost, const double* col_lower, + const double* col_upper, const double* row_lower, + const double* row_upper, const HighsInt* a_start, + const HighsInt* a_index, const double* a_value, + double* col_value, double* col_dual, double* row_value, + double* row_dual, HighsInt* col_basis_status, + HighsInt* row_basis_status, HighsInt* model_status); + + HighsInt Highs_mipCall(const HighsInt num_col, const HighsInt num_row, + const HighsInt num_nz, const HighsInt a_format, + const HighsInt sense, const double offset, + const double* col_cost, const double* col_lower, + const double* col_upper, const double* row_lower, + const double* row_upper, const HighsInt* a_start, + const HighsInt* a_index, const double* a_value, + const HighsInt* integrality, double* col_value, + double* row_value, HighsInt* model_status); + + HighsInt Highs_qpCall( + const HighsInt num_col, const HighsInt num_row, const HighsInt num_nz, + const HighsInt q_num_nz, const HighsInt a_format, const HighsInt q_format, + const HighsInt sense, const double offset, const double* col_cost, + const double* col_lower, const double* col_upper, const double* row_lower, + const double* row_upper, const HighsInt* a_start, const HighsInt* a_index, + const double* a_value, const HighsInt* q_start, const HighsInt* q_index, + const double* q_value, double* col_value, double* col_dual, + double* row_value, double* row_dual, HighsInt* col_basis_status, + HighsInt* row_basis_status, HighsInt* model_status); + + void* Highs_create(void); + + void Highs_destroy(void* highs); + + const char* Highs_version(void); + + HighsInt Highs_versionMajor(void); + + HighsInt Highs_versionMinor(void); + + HighsInt Highs_versionPatch(void); + + const char* Highs_githash(void); + + const char* Highs_compilationDate(void); + + HighsInt Highs_readModel(void* highs, const char* filename); + + HighsInt Highs_writeModel(void* highs, const char* filename); + + HighsInt Highs_clear(void* highs); + + HighsInt Highs_clearModel(void* highs); + + HighsInt Highs_clearSolver(void* highs); + + HighsInt Highs_run(void* highs); + + HighsInt Highs_writeSolution(const void* highs, const char* filename); + + HighsInt Highs_writeSolutionPretty(const void* highs, const char* filename); + + HighsInt Highs_passLp(void* highs, const HighsInt num_col, + const HighsInt num_row, const HighsInt num_nz, + const HighsInt a_format, const HighsInt sense, + const double offset, const double* col_cost, + const double* col_lower, const double* col_upper, + const double* row_lower, const double* row_upper, + const HighsInt* a_start, const HighsInt* a_index, + const double* a_value); + + HighsInt Highs_passMip(void* highs, const HighsInt num_col, + const HighsInt num_row, const HighsInt num_nz, + const HighsInt a_format, const HighsInt sense, + const double offset, const double* col_cost, + const double* col_lower, const double* col_upper, + const double* row_lower, const double* row_upper, + const HighsInt* a_start, const HighsInt* a_index, + const double* a_value, const HighsInt* integrality); + + HighsInt Highs_passModel(void* highs, const HighsInt num_col, + const HighsInt num_row, const HighsInt num_nz, + const HighsInt q_num_nz, const HighsInt a_format, + const HighsInt q_format, const HighsInt sense, + const double offset, const double* col_cost, + const double* col_lower, const double* col_upper, + const double* row_lower, const double* row_upper, + const HighsInt* a_start, const HighsInt* a_index, + const double* a_value, const HighsInt* q_start, + const HighsInt* q_index, const double* q_value, + const HighsInt* integrality); + + HighsInt Highs_passHessian(void* highs, const HighsInt dim, + const HighsInt num_nz, const HighsInt format, + const HighsInt* start, const HighsInt* index, + const double* value); + + HighsInt Highs_passRowName(const void* highs, const HighsInt row, + const char* name); + + HighsInt Highs_passColName(const void* highs, const HighsInt col, + const char* name); + + HighsInt Highs_readOptions(const void* highs, const char* filename); + + HighsInt Highs_setBoolOptionValue(void* highs, const char* option, + const HighsInt value); + + HighsInt Highs_setIntOptionValue(void* highs, const char* option, + const HighsInt value); + + HighsInt Highs_setDoubleOptionValue(void* highs, const char* option, + const double value); + + HighsInt Highs_setStringOptionValue(void* highs, const char* option, + const char* value); + + HighsInt Highs_getBoolOptionValue(const void* highs, const char* option, + HighsInt* value); + + HighsInt Highs_getIntOptionValue(const void* highs, const char* option, + HighsInt* value); + + HighsInt Highs_getDoubleOptionValue(const void* highs, const char* option, + double* value); + + HighsInt Highs_getStringOptionValue(const void* highs, const char* option, + char* value); + + HighsInt Highs_getOptionType(const void* highs, const char* option, + HighsInt* type); + + HighsInt Highs_resetOptions(void* highs); + + HighsInt Highs_writeOptions(const void* highs, const char* filename); + + HighsInt Highs_writeOptionsDeviations(const void* highs, const char* filename); + + HighsInt Highs_getNumOptions(const void* highs); + + HighsInt Highs_getOptionName(const void* highs, const HighsInt index, + char** name); + + HighsInt Highs_getBoolOptionValues(const void* highs, const char* option, + HighsInt* current_value, + HighsInt* default_value); + HighsInt Highs_getIntOptionValues(const void* highs, const char* option, + HighsInt* current_value, HighsInt* min_value, + HighsInt* max_value, HighsInt* default_value); + + HighsInt Highs_getDoubleOptionValues(const void* highs, const char* option, + double* current_value, double* min_value, + double* max_value, double* default_value); + + HighsInt Highs_getStringOptionValues(const void* highs, const char* option, + char* current_value, char* default_value); + + HighsInt Highs_getIntInfoValue(const void* highs, const char* info, + HighsInt* value); + + HighsInt Highs_getDoubleInfoValue(const void* highs, const char* info, + double* value); + + HighsInt Highs_getInt64InfoValue(const void* highs, const char* info, + int64_t* value); + + HighsInt Highs_getInfoType(const void* highs, const char* info, HighsInt* type); + + HighsInt Highs_getSolution(const void* highs, double* col_value, + double* col_dual, double* row_value, + double* row_dual); + + HighsInt Highs_getBasis(const void* highs, HighsInt* col_status, + HighsInt* row_status); + + HighsInt Highs_getModelStatus(const void* highs); + + HighsInt Highs_getDualRay(const void* highs, HighsInt* has_dual_ray, + double* dual_ray_value); + + HighsInt Highs_getPrimalRay(const void* highs, HighsInt* has_primal_ray, + double* primal_ray_value); + + double Highs_getObjectiveValue(const void* highs); + + HighsInt Highs_getBasicVariables(const void* highs, HighsInt* basic_variables); + + HighsInt Highs_getBasisInverseRow(const void* highs, const HighsInt row, + double* row_vector, HighsInt* row_num_nz, + HighsInt* row_index); + + HighsInt Highs_getBasisInverseCol(const void* highs, const HighsInt col, + double* col_vector, HighsInt* col_num_nz, + HighsInt* col_index); + + HighsInt Highs_getBasisSolve(const void* highs, const double* rhs, + double* solution_vector, HighsInt* solution_num_nz, + HighsInt* solution_index); + + HighsInt Highs_getBasisTransposeSolve(const void* highs, const double* rhs, + double* solution_vector, + HighsInt* solution_nz, + HighsInt* solution_index); + + HighsInt Highs_getReducedRow(const void* highs, const HighsInt row, + double* row_vector, HighsInt* row_num_nz, + HighsInt* row_index); + + HighsInt Highs_getReducedColumn(const void* highs, const HighsInt col, + double* col_vector, HighsInt* col_num_nz, + HighsInt* col_index); + + HighsInt Highs_setBasis(void* highs, const HighsInt* col_status, + const HighsInt* row_status); + + HighsInt Highs_setLogicalBasis(void* highs); + + HighsInt Highs_setSolution(void* highs, const double* col_value, + const double* row_value, const double* col_dual, + const double* row_dual); + + HighsInt Highs_setCallback(void* highs, HighsCCallbackType user_callback, + void* user_callback_data); + + HighsInt Highs_startCallback(void* highs, const int callback_type); + + HighsInt Highs_stopCallback(void* highs, const int callback_type); + + double Highs_getRunTime(const void* highs); + + HighsInt Highs_zeroAllClocks(const void* highs); + + HighsInt Highs_addCol(void* highs, const double cost, const double lower, + const double upper, const HighsInt num_new_nz, + const HighsInt* index, const double* value); + + HighsInt Highs_addCols(void* highs, const HighsInt num_new_col, + const double* costs, const double* lower, + const double* upper, const HighsInt num_new_nz, + const HighsInt* starts, const HighsInt* index, + const double* value); + + HighsInt Highs_addVar(void* highs, const double lower, const double upper); + + HighsInt Highs_addVars(void* highs, const HighsInt num_new_var, + const double* lower, const double* upper); + + HighsInt Highs_addRow(void* highs, const double lower, const double upper, + const HighsInt num_new_nz, const HighsInt* index, + const double* value); + + HighsInt Highs_addRows(void* highs, const HighsInt num_new_row, + const double* lower, const double* upper, + const HighsInt num_new_nz, const HighsInt* starts, + const HighsInt* index, const double* value); + + HighsInt Highs_changeObjectiveSense(void* highs, const HighsInt sense); + + HighsInt Highs_changeObjectiveOffset(void* highs, const double offset); + + HighsInt Highs_changeColIntegrality(void* highs, const HighsInt col, + const HighsInt integrality); + + HighsInt Highs_changeColsIntegralityByRange(void* highs, + const HighsInt from_col, + const HighsInt to_col, + const HighsInt* integrality); + + HighsInt Highs_changeColsIntegralityBySet(void* highs, + const HighsInt num_set_entries, + const HighsInt* set, + const HighsInt* integrality); + + HighsInt Highs_changeColsIntegralityByMask(void* highs, const HighsInt* mask, + const HighsInt* integrality); + + HighsInt Highs_changeColCost(void* highs, const HighsInt col, + const double cost); + + HighsInt Highs_changeColsCostByRange(void* highs, const HighsInt from_col, + const HighsInt to_col, const double* cost); + + HighsInt Highs_changeColsCostBySet(void* highs, const HighsInt num_set_entries, + const HighsInt* set, const double* cost); + + HighsInt Highs_changeColsCostByMask(void* highs, const HighsInt* mask, + const double* cost); + + HighsInt Highs_changeColBounds(void* highs, const HighsInt col, + const double lower, const double upper); + + HighsInt Highs_changeColsBoundsByRange(void* highs, const HighsInt from_col, + const HighsInt to_col, + const double* lower, + const double* upper); + + HighsInt Highs_changeColsBoundsBySet(void* highs, + const HighsInt num_set_entries, + const HighsInt* set, const double* lower, + const double* upper); + + HighsInt Highs_changeColsBoundsByMask(void* highs, const HighsInt* mask, + const double* lower, const double* upper); + + HighsInt Highs_changeRowBounds(void* highs, const HighsInt row, + const double lower, const double upper); + + HighsInt Highs_changeRowsBoundsBySet(void* highs, + const HighsInt num_set_entries, + const HighsInt* set, const double* lower, + const double* upper); + + HighsInt Highs_changeRowsBoundsByMask(void* highs, const HighsInt* mask, + const double* lower, const double* upper); + + HighsInt Highs_changeCoeff(void* highs, const HighsInt row, const HighsInt col, + const double value); + + HighsInt Highs_getObjectiveSense(const void* highs, HighsInt* sense); + + HighsInt Highs_getObjectiveOffset(const void* highs, double* offset); + + HighsInt Highs_getColsByRange(const void* highs, const HighsInt from_col, + const HighsInt to_col, HighsInt* num_col, + double* costs, double* lower, double* upper, + HighsInt* num_nz, HighsInt* matrix_start, + HighsInt* matrix_index, double* matrix_value); + + HighsInt Highs_getColsBySet(const void* highs, const HighsInt num_set_entries, + const HighsInt* set, HighsInt* num_col, + double* costs, double* lower, double* upper, + HighsInt* num_nz, HighsInt* matrix_start, + HighsInt* matrix_index, double* matrix_value); + + HighsInt Highs_getColsByMask(const void* highs, const HighsInt* mask, + HighsInt* num_col, double* costs, double* lower, + double* upper, HighsInt* num_nz, + HighsInt* matrix_start, HighsInt* matrix_index, + double* matrix_value); + + HighsInt Highs_getRowsByRange(const void* highs, const HighsInt from_row, + const HighsInt to_row, HighsInt* num_row, + double* lower, double* upper, HighsInt* num_nz, + HighsInt* matrix_start, HighsInt* matrix_index, + double* matrix_value); + + HighsInt Highs_getRowsBySet(const void* highs, const HighsInt num_set_entries, + const HighsInt* set, HighsInt* num_row, + double* lower, double* upper, HighsInt* num_nz, + HighsInt* matrix_start, HighsInt* matrix_index, + double* matrix_value); + + HighsInt Highs_getRowsByMask(const void* highs, const HighsInt* mask, + HighsInt* num_row, double* lower, double* upper, + HighsInt* num_nz, HighsInt* matrix_start, + HighsInt* matrix_index, double* matrix_value); + HighsInt Highs_getRowName(const void* highs, const HighsInt row, char* name); + + HighsInt Highs_getRowByName(const void* highs, const char* name, HighsInt* row); + + HighsInt Highs_getColName(const void* highs, const HighsInt col, char* name); + + HighsInt Highs_getColByName(const void* highs, const char* name, HighsInt* col); + + HighsInt Highs_getColIntegrality(const void* highs, const HighsInt col, + HighsInt* integrality); + + HighsInt Highs_deleteColsByRange(void* highs, const HighsInt from_col, + const HighsInt to_col); + + HighsInt Highs_deleteColsBySet(void* highs, const HighsInt num_set_entries, + const HighsInt* set); + + HighsInt Highs_deleteColsByMask(void* highs, HighsInt* mask); + + HighsInt Highs_deleteRowsByRange(void* highs, const int from_row, + const HighsInt to_row); + + HighsInt Highs_deleteRowsBySet(void* highs, const HighsInt num_set_entries, + const HighsInt* set); + + HighsInt Highs_deleteRowsByMask(void* highs, HighsInt* mask); + + HighsInt Highs_scaleCol(void* highs, const HighsInt col, const double scaleval); + + HighsInt Highs_scaleRow(void* highs, const HighsInt row, const double scaleval); + + double Highs_getInfinity(const void* highs); + + HighsInt Highs_getSizeofHighsInt(const void* highs); + + HighsInt Highs_getNumCol(const void* highs); + + HighsInt Highs_getNumRow(const void* highs); + + HighsInt Highs_getNumNz(const void* highs); + + HighsInt Highs_getHessianNumNz(const void* highs); + + HighsInt Highs_getModel(const void* highs, const HighsInt a_format, + const HighsInt q_format, HighsInt* num_col, + HighsInt* num_row, HighsInt* num_nz, + HighsInt* hessian_num_nz, HighsInt* sense, + double* offset, double* col_cost, double* col_lower, + double* col_upper, double* row_lower, double* row_upper, + HighsInt* a_start, HighsInt* a_index, double* a_value, + HighsInt* q_start, HighsInt* q_index, double* q_value, + HighsInt* integrality); + + HighsInt Highs_crossover(void* highs, const int num_col, const int num_row, + const double* col_value, const double* col_dual, + const double* row_dual); + + HighsInt Highs_getRanging(void* highs, + double* col_cost_up_value, double* col_cost_up_objective, + HighsInt* col_cost_up_in_var, HighsInt* col_cost_up_ou_var, + double* col_cost_dn_value, double* col_cost_dn_objective, + HighsInt* col_cost_dn_in_var, HighsInt* col_cost_dn_ou_var, + double* col_bound_up_value, double* col_bound_up_objective, + HighsInt* col_bound_up_in_var, HighsInt* col_bound_up_ou_var, + double* col_bound_dn_value, double* col_bound_dn_objective, + HighsInt* col_bound_dn_in_var, HighsInt* col_bound_dn_ou_var, + double* row_bound_up_value, double* row_bound_up_objective, + HighsInt* row_bound_up_in_var, HighsInt* row_bound_up_ou_var, + double* row_bound_dn_value, double* row_bound_dn_objective, + HighsInt* row_bound_dn_in_var, HighsInt* row_bound_dn_ou_var); + + void Highs_resetGlobalScheduler(const HighsInt blocking); + + // ********************* + // * Deprecated methods* + // ********************* + + const HighsInt HighsStatuskError = -1; + const HighsInt HighsStatuskOk = 0; + const HighsInt HighsStatuskWarning = 1; + + HighsInt Highs_call(const HighsInt num_col, const HighsInt num_row, + const HighsInt num_nz, const double* col_cost, + const double* col_lower, const double* col_upper, + const double* row_lower, const double* row_upper, + const HighsInt* a_start, const HighsInt* a_index, + const double* a_value, double* col_value, double* col_dual, + double* row_value, double* row_dual, + HighsInt* col_basis_status, HighsInt* row_basis_status, + HighsInt* model_status); + + HighsInt Highs_runQuiet(void* highs); + + HighsInt Highs_setHighsLogfile(void* highs, const void* logfile); + + HighsInt Highs_setHighsOutput(void* highs, const void* outputfile); + + HighsInt Highs_getIterationCount(const void* highs); + + HighsInt Highs_getSimplexIterationCount(const void* highs); + + HighsInt Highs_setHighsBoolOptionValue(void* highs, const char* option, + const HighsInt value); + + HighsInt Highs_setHighsIntOptionValue(void* highs, const char* option, + const HighsInt value); + + HighsInt Highs_setHighsDoubleOptionValue(void* highs, const char* option, + const double value); + + HighsInt Highs_setHighsStringOptionValue(void* highs, const char* option, + const char* value); + + HighsInt Highs_setHighsOptionValue(void* highs, const char* option, + const char* value); + + HighsInt Highs_getHighsBoolOptionValue(const void* highs, const char* option, + HighsInt* value); + + HighsInt Highs_getHighsIntOptionValue(const void* highs, const char* option, + HighsInt* value); + + HighsInt Highs_getHighsDoubleOptionValue(const void* highs, const char* option, + double* value); + + HighsInt Highs_getHighsStringOptionValue(const void* highs, const char* option, + char* value); + + HighsInt Highs_getHighsOptionType(const void* highs, const char* option, + HighsInt* type); + + HighsInt Highs_resetHighsOptions(void* highs); + + HighsInt Highs_getHighsIntInfoValue(const void* highs, const char* info, + HighsInt* value); + + HighsInt Highs_getHighsDoubleInfoValue(const void* highs, const char* info, + double* value); + + HighsInt Highs_getNumCols(const void* highs); + + HighsInt Highs_getNumRows(const void* highs); + + double Highs_getHighsInfinity(const void* highs); + + double Highs_getHighsRunTime(const void* highs); + + HighsInt Highs_setOptionValue(void* highs, const char* option, + const char* value); + + HighsInt Highs_getScaledModelStatus(const void* highs); + """ + ) STATUS_ERROR = highslib.kHighsStatusError - def check(status): - "Check return status and raise error if not OK." - if status == STATUS_ERROR: - raise mip.InterfacingError("Unknown error in call to HiGHS.") + +def check(status): + if status == STATUS_ERROR: + raise mip.InterfacingError("Unknown error in call to HiGHS.") class SolverHighs(mip.Solver): @@ -202,18 +705,26 @@ def __init__(self, model: mip.Model, name: str, sense: str): # Store additional data here, if HiGHS can't do it. self._name: str = name - self._var_name: List[str] = [] - self._var_col: Dict[str, int] = {} - self._var_type: List[str] = [] - self._cons_name: List[str] = [] - self._cons_col: Dict[str, int] = {} + self._num_int_vars = 0 # Also store solution (when available) self._x = [] self._rc = [] self._pi = [] + # Buffer string for storing names + self._name_buffer = ffi.new(f"char[{self._lib.kHighsMaximumStringLength}]") + + # type conversion maps + self._var_type_map = { + mip.CONTINUOUS: self._lib.kHighsVarTypeContinuous, + mip.BINARY: self._lib.kHighsVarTypeInteger, + mip.INTEGER: self._lib.kHighsVarTypeInteger, + } + self._highs_type_map = {value: key for key, value in self._var_type_map.items()} + def __del__(self): + self._name_buffer = None self._lib.Highs_destroy(self._model) def _get_int_info_value(self: "SolverHighs", name: str) -> int: @@ -295,9 +806,11 @@ def add_var( name: str = "", ): col: int = self.num_cols() - check(self._lib.Highs_addVar(self._model, lb, ub)) - check(self._lib.Highs_changeColCost(self._model, col, obj)) + check(self._lib.Highs_addCol(self._model, obj, lb, ub, 0, ffi.NULL, ffi.NULL)) + if name: + check(self._lib.Highs_passColName(self._model, col, name.encode("utf-8"))) if var_type != mip.CONTINUOUS: + self._num_int_vars += 1 check( self._lib.Highs_changeColIntegrality( self._model, col, self._lib.kHighsVarTypeInteger @@ -311,11 +824,6 @@ def add_var( for cons, coef in zip(column.constrs, column.coeffs): self._change_coef(cons.idx, col, coef) - # store name & type - self._var_name.append(name) - self._var_col[name] = col - self._var_type.append(var_type) - def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): row: int = self.num_rows() @@ -336,10 +844,8 @@ def add_constr(self: "SolverHighs", lin_expr: "mip.LinExpr", name: str = ""): check( self._lib.Highs_addRow(self._model, lower, upper, num_new_nz, index, value) ) - - # store name - self._cons_name.append(name) - self._cons_col[name] = row + if name: + self._lib.Highs_passRowName(self._model, row, name.encode("utf-8")) def add_lazy_constr(self: "SolverHighs", lin_expr: "mip.LinExpr"): raise NotImplementedError("HiGHS doesn't support lazy constraints!") @@ -393,33 +899,27 @@ def get_objective_const(self: "SolverHighs") -> numbers.Real: def _all_cols_continuous(self: "SolverHighs"): n = self.num_cols() - integrality = ffi.new( - "int[]", [self._lib.kHighsVarTypeContinuous for i in range(n)] - ) + self._num_int_vars = 0 + integrality = ffi.new("int[]", [self._lib.kHighsVarTypeContinuous] * n) check( self._lib.Highs_changeColsIntegralityByRange( self._model, 0, n - 1, integrality ) ) - def _reset_var_types(self: "SolverHighs"): - var_type_map = { - mip.CONTINUOUS: self._lib.kHighsVarTypeContinuous, - mip.BINARY: self._lib.kHighsVarTypeInteger, - mip.INTEGER: self._lib.kHighsVarTypeInteger, - } - integrality = ffi.new("int[]", [var_type_map[vt] for vt in self._var_type]) + def _reset_var_types(self: "SolverHighs", var_types: List[str]): + integrality = ffi.new("int[]", [self._var_type_map[vt] for vt in var_types]) n = self.num_cols() check( self._lib.Highs_changeColsIntegralityByRange( self._model, 0, n - 1, integrality ) ) + self._num_int_vars = sum(1 for vt in var_types if vt != mip.CONTINUOUS) def relax(self: "SolverHighs"): # change integrality of all columns self._all_cols_continuous() - self._var_type = [mip.CONTINUOUS] * len(self._var_type) def generate_cuts( self, @@ -439,20 +939,25 @@ def optimize( relax: bool = False, ) -> "mip.OptimizationStatus": if relax: - # Temporarily change variable types. Original types are still stored - # in self._var_type. + # Temporarily change variable types. + # Original types are stored in list var_type. + var_types: List[str] = [var.var_type for var in self.model.vars] self._all_cols_continuous() self.set_mip_gap(self.model.max_mip_gap) self.set_mip_gap_abs(self.model.max_mip_gap_abs) - + check(self._lib.Highs_run(self._model)) # check whether unsupported callbacks were set if self.model.lazy_constrs_generator: - raise NotImplementedError("HiGHS doesn't support lazy constraints at the moment") + raise NotImplementedError( + "HiGHS doesn't support lazy constraints at the moment" + ) if self.model.cuts_generator: - raise NotImplementedError("HiGHS doesn't support cuts generator at the moment") + raise NotImplementedError( + "HiGHS doesn't support cuts generator at the moment" + ) # store solution values for later access opt_status = self.get_status() @@ -478,7 +983,7 @@ def optimize( if relax: # Undo the temporary changes. - self._reset_var_types() + self._reset_var_types(var_types) return opt_status @@ -592,6 +1097,8 @@ def write(self: "SolverHighs", file_path: str): check(self._lib.Highs_writeModel(self._model, file_path.encode("utf-8"))) def read(self: "SolverHighs", file_path: str): + if file_path.lower().endswith(".bas"): + raise NotImplementedError("HiGHS does not support bas files") check(self._lib.Highs_readModel(self._model, file_path.encode("utf-8"))) def num_cols(self: "SolverHighs") -> int: @@ -604,7 +1111,7 @@ def num_nz(self: "SolverHighs") -> int: return self._lib.Highs_getNumNz(self._model) def num_int(self: "SolverHighs") -> int: - return sum(vt != mip.CONTINUOUS for vt in self._var_type) + return self._num_int_vars def get_emphasis(self: "SolverHighs") -> mip.SearchEmphasis: raise NotImplementedError("HiGHS doesn't support search emphasis.") @@ -783,7 +1290,9 @@ def constr_set_rhs(self: "SolverHighs", idx: int, rhs: numbers.Real): check(self._lib.Highs_changeRowBounds(self._model, idx, lb, ub)) def constr_get_name(self: "SolverHighs", idx: int) -> str: - return self._cons_name[idx] + name = self._name_buffer + check(self._lib.Highs_getRowName(self._model, idx, name)) + return ffi.string(name).decode("utf-8") def constr_get_pi(self: "SolverHighs", constr: "mip.Constr") -> numbers.Real: if self._pi: @@ -808,7 +1317,9 @@ def remove_constrs(self: "SolverHighs", constrsList: List[int]): check(self._lib.Highs_deleteRowsBySet(self._model, len(constrsList), set_)) def constr_get_index(self: "SolverHighs", name: str) -> int: - return self._cons_col[name] + idx = ffi.new("int *") + self._lib.Highs_getRowByName(self._model, name.encode("utf-8"), idx) + return idx[0] # Variable-related getters/setters @@ -906,16 +1417,28 @@ def var_set_obj(self: "SolverHighs", var: "mip.Var", value: numbers.Real): check(self._lib.Highs_changeColCost(self._model, var.idx, value)) def var_get_var_type(self: "SolverHighs", var: "mip.Var") -> str: - return self._var_type[var.idx] + var_type = ffi.new("int*") + ret = self._lib.Highs_getColIntegrality(self._model, var.idx, var_type) + if var_type[0] not in self._highs_type_map: + raise ValueError( + f"Invalid variable type returned by HiGHS: {var_type[0]} (ret={ret})" + ) + return self._highs_type_map[var_type[0]] def var_set_var_type(self: "SolverHighs", var: "mip.Var", value: str): - if value != mip.CONTINUOUS: + if value not in self._var_type_map: + raise ValueError(f"Invalid variable type: {value}") + prev_var_type = var.var_type + if value != prev_var_type: check( self._lib.Highs_changeColIntegrality( - self._model, var.idx, self._lib.kHighsVarTypeInteger + self._model, var.idx, self._var_type_map[value] ) ) - self._var_type[var.idx] = value + if prev_var_type != mip.CONTINUOUS and value == mip.CONTINUOUS: + self._num_int_vars -= 1 + elif prev_var_type == mip.CONTINUOUS and value != mip.CONTINUOUS: + self._num_int_vars += 1 def var_get_column(self: "SolverHighs", var: "mip.Var") -> "mip.Column": # Call method twice: @@ -980,14 +1503,18 @@ def var_get_xi(self: "SolverHighs", var: "mip.Var", i: int) -> numbers.Real: raise NotImplementedError("HiGHS doesn't store multiple solutions.") def var_get_name(self: "SolverHighs", idx: int) -> str: - return self._var_name[idx] + name = self._name_buffer + check(self._lib.Highs_getColName(self._model, idx, name)) + return ffi.string(name).decode("utf-8") def remove_vars(self: "SolverHighs", varsList: List[int]): set_ = ffi.new("int[]", varsList) check(self._lib.Highs_deleteColsBySet(self._model, len(varsList), set_)) def var_get_index(self: "SolverHighs", name: str) -> int: - return self._var_col[name] + idx = ffi.new("int *") + self._lib.Highs_getColByName(self._model, name.encode("utf-8"), idx) + return idx[0] def get_problem_name(self: "SolverHighs") -> str: return self._name