Skip to content

Commit

Permalink
Merge pull request Pyomo#2971 from ZedongPeng/add_highs_support
Browse files Browse the repository at this point in the history
Add highs support in MindtPy
  • Loading branch information
blnicho authored May 7, 2024
2 parents e77be8c + c610a20 commit 6e08021
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 38 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test_pr_and_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions pyomo/contrib/appsi/solvers/highs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 43 additions & 25 deletions pyomo/contrib/mindtpy/algorithm_base_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions pyomo/contrib/mindtpy/config_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
7 changes: 6 additions & 1 deletion pyomo/contrib/mindtpy/tests/test_mindtpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 7 additions & 2 deletions pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 6e08021

Please sign in to comment.