diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 5b1bca70ede..864c19c6d73 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -612,7 +612,8 @@ jobs: if: ${{ ! matrix.slim }} shell: bash run: | - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy \ + echo "NOTE: temporarily pinning to highspy pre-release for testing" + $PYTHON_EXE -m pip install --cache-dir cache/pip "highspy>=1.7.1.dev1" \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 6410700c569..c948444839d 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -176,11 +176,19 @@ def available(self): return self.Availability.NotFound def version(self): - version = ( - highspy.HIGHS_VERSION_MAJOR, - highspy.HIGHS_VERSION_MINOR, - highspy.HIGHS_VERSION_PATCH, - ) + try: + version = ( + highspy.HIGHS_VERSION_MAJOR, + highspy.HIGHS_VERSION_MINOR, + highspy.HIGHS_VERSION_PATCH, + ) + except AttributeError: + # Older versions of Highs do not have the above attributes + # and the solver version can only be obtained by making + # an instance of the solver class. + tmp = highspy.Highs() + version = (tmp.versionMajor(), tmp.versionMinor(), tmp.versionPatch()) + return version @property diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 8c703f8d842..380051e93ca 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -146,7 +146,9 @@ def __init__(self, **kwds): # Store the OA cuts generated in the mip_start_process. self.mip_start_lazy_oa_cuts = [] # Whether to load solutions in solve() function - self.load_solutions = True + self.mip_load_solutions = True + self.nlp_load_solutions = True + self.regularization_mip_load_solutions = True # Support use as a context manager under current solver API def __enter__(self): @@ -296,7 +298,7 @@ def model_is_valid(self): results = self.mip_opt.solve( self.original_model, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(results.solution) > 0: @@ -838,7 +840,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -860,7 +862,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -991,7 +993,10 @@ def init_max_binaries(self): mip_args = dict(config.mip_solver_args) update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) results = self.mip_opt.solve( - m, tee=config.mip_solver_tee, load_solutions=self.load_solutions, **mip_args + m, + tee=config.mip_solver_tee, + load_solutions=self.mip_load_solutions, + **mip_args, ) if len(results.solution) > 0: m.solutions.load_from(results) @@ -1111,7 +1116,7 @@ def solve_subproblem(self): results = self.nlp_opt.solve( self.fixed_nlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -1379,12 +1384,20 @@ def solve_feasibility_subproblem(self): update_solver_timelimit( self.feasibility_nlp_opt, config.nlp_solver, self.timing, config ) - TransformationFactory('contrib.deactivate_trivial_constraints').apply_to( - feas_subproblem, - tmp=True, - ignore_infeasible=False, - tolerance=config.constraint_tolerance, - ) + try: + TransformationFactory('contrib.deactivate_trivial_constraints').apply_to( + self.fixed_nlp, + tmp=True, + ignore_infeasible=False, + tolerance=config.constraint_tolerance, + ) + except InfeasibleConstraintException as e: + config.logger.error( + str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + ) + results = SolverResults() + results.solver.termination_condition = tc.infeasible + return self.fixed_nlp, results with SuppressInfeasibleWarning(): try: with time_code(self.timing, 'feasibility subproblem'): @@ -1570,7 +1583,7 @@ def fix_dual_bound(self, last_iter_cuts): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) if len(main_mip_results.solution) > 0: @@ -1658,7 +1671,7 @@ def solve_main(self): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. @@ -1719,7 +1732,7 @@ def solve_fp_main(self): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. @@ -1762,7 +1775,7 @@ def solve_regularization_main(self): main_mip_results = self.regularization_mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.regularization_mip_load_solutions, **dict(config.mip_solver_args), ) if len(main_mip_results.solution) > 0: @@ -1978,7 +1991,7 @@ def handle_main_unbounded(self, main_mip): main_mip_results = self.mip_opt.solve( main_mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(main_mip_results.solution) > 0: @@ -2261,6 +2274,11 @@ def check_subsolver_validity(self): raise ValueError(self.config.mip_solver + ' is not available.') if not self.mip_opt.license_is_valid(): raise ValueError(self.config.mip_solver + ' is not licensed.') + if self.config.mip_solver == "appsi_highs": + if self.mip_opt.version() < (1, 7, 0): + raise ValueError( + "MindtPy requires the use of HIGHS version 1.7.0 or higher for full compatibility." + ) if not self.nlp_opt.available(): raise ValueError(self.config.nlp_solver + ' is not available.') if not self.nlp_opt.license_is_valid(): @@ -2308,15 +2326,15 @@ def check_config(self): config.mip_solver = 'cplex_persistent' # related to https://github.com/Pyomo/pyomo/issues/2363 + if 'appsi' in config.mip_solver: + self.mip_load_solutions = False + if 'appsi' in config.nlp_solver: + self.nlp_load_solutions = False if ( - 'appsi' in config.mip_solver - or 'appsi' in config.nlp_solver - or ( - config.mip_regularization_solver is not None - and 'appsi' in config.mip_regularization_solver - ) + config.mip_regularization_solver is not None + and 'appsi' in config.mip_regularization_solver ): - self.load_solutions = False + self.regularization_mip_load_solutions = False ################################################################################################################################ # Feasibility Pump @@ -2384,7 +2402,7 @@ def solve_fp_subproblem(self): results = self.nlp_opt.solve( fp_nlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index ba2b74cdfe0..b2808952c28 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -549,7 +549,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', TODO: feasibility pump now fails with appsi_highs #2951 + 'appsi_highs', ] ), description='MIP subsolver name', @@ -631,7 +631,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', + 'appsi_highs', ] ), description='MIP subsolver for regularization problem', diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index 37969276d55..d0364378ed8 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -56,7 +56,12 @@ QCP_model._generate_model() extreme_model_list = [LP_model.model, QCP_model.model] -required_solvers = ('ipopt', 'glpk') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index 24679047793..dda0f74147e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -23,7 +23,13 @@ from pyomo.environ import SolverFactory, value from pyomo.opt import TerminationCondition -required_solvers = ('ipopt', 'glpk') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index cbc906851bf..0baa361910e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -28,8 +28,13 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'glpk') -# TODO: 'appsi_highs' will fail here. +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py index d50a41ad000..e01558d48ef 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py @@ -18,7 +18,14 @@ from pyomo.contrib.mindtpy.tests.MINLP_simple import SimpleMINLP as SimpleMINLP model_list = [SimpleMINLP(grey_box=True)] -required_solvers = ('cyipopt', 'glpk') + +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('cyipopt', 'appsi_highs') +else: + required_solvers = ('cyipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: