Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add highs support in MindtPy #2971

Merged
merged 19 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" \
Comment on lines +615 to +616
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove 'temporarily' if this is how we're leaving it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should leave it for now. When they cut a new (non-dev) highspy release we can remove the version requirement.

|| 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 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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(

Check warning on line 1395 in pyomo/contrib/mindtpy/algorithm_base_class.py

View check run for this annotation

Codecov / codecov/patch

pyomo/contrib/mindtpy/algorithm_base_class.py#L1394-L1395

Added lines #L1394 - L1395 were not covered by tests
str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.'
)
results = SolverResults()
results.solver.termination_condition = tc.infeasible
return self.fixed_nlp, results

Check warning on line 1400 in pyomo/contrib/mindtpy/algorithm_base_class.py

View check run for this annotation

Codecov / codecov/patch

pyomo/contrib/mindtpy/algorithm_base_class.py#L1398-L1400

Added lines #L1398 - L1400 were not covered by tests
with SuppressInfeasibleWarning():
try:
with time_code(self.timing, 'feasibility subproblem'):
Expand Down Expand Up @@ -1570,7 +1583,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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(

Check warning on line 2279 in pyomo/contrib/mindtpy/algorithm_base_class.py

View check run for this annotation

Codecov / codecov/patch

pyomo/contrib/mindtpy/algorithm_base_class.py#L2279

Added line #L2279 was not covered by tests
"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 @@
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

Check warning on line 2337 in pyomo/contrib/mindtpy/algorithm_base_class.py

View check run for this annotation

Codecov / codecov/patch

pyomo/contrib/mindtpy/algorithm_base_class.py#L2337

Added line #L2337 was not covered by tests

################################################################################################################################
# Feasibility Pump
Expand Down Expand Up @@ -2384,7 +2402,7 @@
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
Loading