diff --git a/pyomo/contrib/appsi/solvers/copt.py b/pyomo/contrib/appsi/solvers/copt.py index aa558d6c477..0f3809637cb 100644 --- a/pyomo/contrib/appsi/solvers/copt.py +++ b/pyomo/contrib/appsi/solvers/copt.py @@ -2,11 +2,13 @@ import math from typing import List, Dict, Optional from pyomo.common.collections import ComponentMap, OrderedSet +from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException +from pyomo.common.tee import capture_output, TeeStream from pyomo.common.timing import HierarchicalTimer from pyomo.common.shutdown import python_is_shutting_down -from pyomo.common.config import ConfigValue +from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import _GeneralVarData @@ -26,22 +28,24 @@ ) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager +import sys logger = logging.getLogger(__name__) -coptpy_available = False -try: - import coptpy +def _import_coptpy(): + try: + import coptpy + except ImportError: + Copt._available = Copt.Availability.NotFound + raise + if coptpy.COPT.VERSION_MAJOR < 6: + Copt._available = Copt.Availability.BadVersion + raise ImportError('The APPSI COPT interface requires coptpy>=6.0.0') + return coptpy - if not ( - coptpy.COPT.VERSION_MAJOR > 6 - or (coptpy.COPT.VERSION_MAJOR == 6 and coptpy.COPT.VERSION_MINOR >= 5) - ): - raise ImportError('The APPSI Copt interface requires coptpy>=6.5.0') - coptpy_available = True -except: - pass + +coptpy, coptpy_available = attempt_import('coptpy', importer=_import_coptpy) class DegreeError(PyomoException): @@ -66,7 +70,12 @@ def __init__( ) self.declare('logfile', ConfigValue(domain=str)) + self.declare('solver_output_logger', ConfigValue()) + self.declare('log_level', ConfigValue(domain=NonNegativeInt)) + self.logfile = '' + self.solver_output_logger = logger + self.log_level = logging.INFO class CoptSolutionLoader(PersistentSolutionLoader): @@ -226,6 +235,7 @@ class Copt(PersistentBase, PersistentSolver): _available = None _num_instances = 0 + _coptenv = None def __init__(self, only_child_vars=True): super(Copt, self).__init__(only_child_vars=only_child_vars) @@ -235,10 +245,8 @@ def __init__(self, only_child_vars=True): self._solver_options = dict() self._solver_model = None - if coptpy_available: + if coptpy_available and self._coptenv is None: self._coptenv = coptpy.Envr() - else: - self._coptenv = None self._symbol_map = SymbolMap() self._labeler = None @@ -256,7 +264,12 @@ def __init__(self, only_child_vars=True): self._last_results_object: Optional[CoptResults] = None def available(self): - if self._available is None: + if not coptpy_available: + return self.Availability.NotFound + elif self._available == self.Availability.BadVersion: + return self.Availability.BadVersion + else: + self._available = Copt.Availability.BadLicense if self._coptenv is not None: m = self._coptenv.createModel('checklic') m.setParam("Logging", 0) @@ -267,7 +280,7 @@ def available(self): self._available = Copt.Availability.FullLicense except coptpy.CoptError: self._available = Copt.Availability.LimitedLicense - return self._available + return self._available def release_license(self): self._reinit() @@ -316,25 +329,37 @@ def symbol_map(self): return self._symbol_map def _solve(self, timer: HierarchicalTimer): - config = self.config - options = self.copt_options - if config.stream_solver: - self._solver_model.setParam('LogToConsole', 1) - else: - self._solver_model.setParam('LogToConsole', 0) - self._solver_model.setLogFile(config.logfile) - - if config.time_limit is not None: - self._solver_model.setParam('TimeLimit', config.time_limit) - if config.mip_gap is not None: - self._solver_model.setParam('RelGap', config.mip_gap) - - for key, option in options.items(): - self._solver_model.setParam(key, option) - timer.start('solve') - self._solver_model.solve(self._callback) - timer.stop('solve') - self._needs_updated = False + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.stream_solver: + ostreams.append(sys.stdout) + + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + config = self.config + options = self.copt_options + + if config.stream_solver: + self._solver_model.setParam('LogToConsole', 1) + else: + self._solver_model.setParam('LogToConsole', 0) + self._solver_model.setLogFile(config.logfile) + + if config.time_limit is not None: + self._solver_model.setParam('TimeLimit', config.time_limit) + if config.mip_gap is not None: + self._solver_model.setParam('RelGap', config.mip_gap) + + for key, option in options.items(): + self._solver_model.setParam(key, option) + + timer.start('solve') + self._solver_model.solve() + timer.stop('solve') + return self._postsolve(timer) def solve(self, model, timer: HierarchicalTimer = None) -> Results: diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index c24af0ffbe7..c1c0876b203 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -531,7 +531,6 @@ def _add_subsolver_configs(CONFIG): [ 'gurobi', 'cplex', - 'copt', 'cbc', 'glpk', 'gams', @@ -616,7 +615,6 @@ def _add_subsolver_configs(CONFIG): [ 'gurobi', 'cplex', - 'copt', 'cbc', 'glpk', 'gams', diff --git a/pyomo/solvers/plugins/solvers/copt_direct.py b/pyomo/solvers/plugins/solvers/copt_direct.py index 6f21de8f17b..c7893f8f193 100644 --- a/pyomo/solvers/plugins/solvers/copt_direct.py +++ b/pyomo/solvers/plugins/solvers/copt_direct.py @@ -14,6 +14,7 @@ import sys from pyomo.common.collections import ComponentSet, ComponentMap, Bunch +from pyomo.common.dependencies import attempt_import from pyomo.common.errors import ApplicationError from pyomo.common.tempfiles import TempfileManager from pyomo.core.expr.numvalue import value, is_fixed @@ -32,21 +33,35 @@ logger = logging.getLogger('pyomo.solvers') -coptpy_available = False -try: - import coptpy - coptpy_available = True -except: +class DegreeError(ValueError): pass -class DegreeError(ValueError): - pass +def _parse_coptpy_version(coptpy, avail): + if not avail: + return + coptpy_major = coptpy.COPT.VERSION_MAJOR + coptpy_minor = coptpy.COPT.VERSION_MINOR + coptpy_tech = coptpy.COPT.VERSION_TECHNICAL + CoptDirect._version = (coptpy_major, coptpy_minor, coptpy_tech) + CoptDirect._name = "COPT %s.%s.%s" % CoptDirect._version + while len(CoptDirect._version) < 4: + CoptDirect._version += (0,) + CoptDirect._version = CoptDirect._version[:4] + + +coptpy, coptpy_available = attempt_import( + 'coptpy', catch_exceptions=(Exception,), callback=_parse_coptpy_version +) @SolverFactory.register('copt_direct', doc='Direct python interface to COPT') class CoptDirect(DirectSolver): + _name = None + _version = 0 + _coptenv = None + def __init__(self, **kwds): if 'type' not in kwds: kwds['type'] = 'copt_direct' @@ -55,21 +70,6 @@ def __init__(self, **kwds): self._python_api_exists = True - self._version_major = coptpy.COPT.VERSION_MAJOR - self._version_minor = coptpy.COPT.VERSION_MINOR - self._version_technical = coptpy.COPT.VERSION_TECHNICAL - self._version_name = "COPT %s.%s.%s" % ( - self._version_major, - self._version_minor, - self._version_technical, - ) - - if coptpy_available: - self._coptenv = coptpy.Envr() - else: - self._coptenv = None - self._coptmodel_name = "coptprob" - self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = dict() @@ -85,6 +85,11 @@ def __init__(self, **kwds): self._capabilities.sos1 = True self._capabilities.sos2 = True + if coptpy_available and self._coptenv is None: + self._coptenv = coptpy.Envr() + self._coptmodel_name = "coptprob" + self._solver_model = None + def available(self, exception_flag=True): if not coptpy_available: if exception_flag: @@ -338,6 +343,9 @@ def _set_instance(self, model, kwds={}): self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() + if self._solver_model is not None: + self._solver_model.clear() + self._solver_model = None try: if model.name is not None: self._solver_model = self._coptenv.createModel(model.name) @@ -403,7 +411,7 @@ def _postsolve(self): self.results = SolverResults() soln = Solution() - self.results.solver.name = self._version_name + self.results.solver.name = self._name self.results.solver.wallclock_time = self._solver_model.SolvingTime status = self._solver_model.status diff --git a/pyomo/solvers/plugins/solvers/copt_persistent.py b/pyomo/solvers/plugins/solvers/copt_persistent.py index 2ba231d70b7..1f991563932 100644 --- a/pyomo/solvers/plugins/solvers/copt_persistent.py +++ b/pyomo/solvers/plugins/solvers/copt_persistent.py @@ -9,13 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.solvers.plugins.solvers.copt_direct import CoptDirect, coptpy_available +from pyomo.solvers.plugins.solvers.copt_direct import CoptDirect, coptpy from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver from pyomo.opt.base import SolverFactory -if coptpy_available: - import coptpy - @SolverFactory.register('copt_persistent', doc='Persistent python interface to COPT') class CoptPersistent(PersistentSolver, CoptDirect): diff --git a/pyomo/solvers/tests/checks/test_copt_persistent.py b/pyomo/solvers/tests/checks/test_copt_persistent.py index ef0112ed162..b5b67ff649e 100644 --- a/pyomo/solvers/tests/checks/test_copt_persistent.py +++ b/pyomo/solvers/tests/checks/test_copt_persistent.py @@ -12,12 +12,13 @@ import pyomo.common.unittest as unittest import pyomo.environ as pyo +coptpy_available = False try: import coptpy coptpy_available = True except: - coptpy_available = False + pass class TestCoptPersistent(unittest.TestCase): diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index e45718857ba..f998fd2ddeb 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -440,8 +440,8 @@ def test_solver_cases(*args): ] ) - _test_solver_cases['copt', 'python'] = initialize( - name='copt', + _test_solver_cases['copt_direct', 'python'] = initialize( + name='copt_direct', io='python', capabilities=_copt_capabilities, import_suffixes=['slack', 'dual', 'rc'],