Repository: Pyomo/pyomo

Issue:
`CPLEXDirect` (and `CPLEXPersistent`) do not support v12.10
1. Install `cplex==12.10.0`
2. Run a model using the persistent solver
3. The following error occurs

```
  File "...venv/lib/python3.7/site-packages/cplex/_internal/_ostream.py", line 66, in __init__
    raise TypeError("outputfile must have a write method")
TypeError: outputfile must have a write method
```

This is due to `pyomo/solvers/plugins/solvers/cplex_direct.py` line 112, (in `_apply_solver`) using a call to `set_results_stream()` that is now deprecated. A deprecation warning was raised in version 12.9.

The fix looks pretty straightforward; `CPLEXDirect._log_file` is a string path whereas `Cplex.set_results_stream()` expects a file-like object.


Discussion:
@ruaridhw if you have an idea for how to fix this PRs are always welcome. We should make sure any fix for this is also backwards compatible with older versions of CPLEX.

Resolving patch:
diff --git a/pyomo/solvers/plugins/solvers/cplex_direct.py b/pyomo/solvers/plugins/solvers/cplex_direct.py
--- a/pyomo/solvers/plugins/solvers/cplex_direct.py
+++ b/pyomo/solvers/plugins/solvers/cplex_direct.py
@@ -105,82 +105,89 @@ def _apply_solver(self):
             for block in self._pyomo_model.block_data_objects(descend_into=True, active=True):
                 for var in block.component_data_objects(ctype=pyomo.core.base.var.Var, descend_into=False, active=True, sort=False):
                     var.stale = True
-        if self._tee:
-            def _process_stream(arg):
-                sys.stdout.write(arg)
-                return arg
-            self._solver_model.set_results_stream(self._log_file, _process_stream)
-        else:
-            self._solver_model.set_results_stream(self._log_file)
-
-        if self._keepfiles:
-            print("Solver log file: "+self._log_file)
-
-        obj_degree = self._objective.expr.polynomial_degree()
-        if obj_degree is None or obj_degree > 2:
-            raise DegreeError('CPLEXDirect does not support expressions of degree {0}.'\
-                              .format(obj_degree))
-        elif obj_degree == 2:
-            quadratic_objective = True
-        else:
-            quadratic_objective = False
-
-        num_integer_vars = self._solver_model.variables.get_num_integer()
-        num_binary_vars = self._solver_model.variables.get_num_binary()
-        num_sos = self._solver_model.SOS.get_num()
-
-        if self._solver_model.quadratic_constraints.get_num() != 0:
-            quadratic_cons = True
-        else:
-            quadratic_cons = False
-
-        if (num_integer_vars + num_binary_vars + num_sos) > 0:
-            integer = True
-        else:
-            integer = False
-
-        if integer:
-            if quadratic_cons:
-                self._solver_model.set_problem_type(self._solver_model.problem_type.MIQCP)
-            elif quadratic_objective:
-                self._solver_model.set_problem_type(self._solver_model.problem_type.MIQP)
+        _log_file = self._log_file
+        if self.version() >= (12, 10):
+            _log_file = open(self._log_file, 'w')
+        try:
+            if self._tee:
+                def _process_stream(arg):
+                    sys.stdout.write(arg)
+                    return arg
+                self._solver_model.set_results_stream(_log_file, _process_stream)
             else:
-                self._solver_model.set_problem_type(self._solver_model.problem_type.MILP)
-        else:
-            if quadratic_cons:
-                self._solver_model.set_problem_type(self._solver_model.problem_type.QCP)
-            elif quadratic_objective:
-                self._solver_model.set_problem_type(self._solver_model.problem_type.QP)
+                self._solver_model.set_results_stream(_log_file)
+            
+            if self._keepfiles:
+                print("Solver log file: "+self._log_file)
+            
+            obj_degree = self._objective.expr.polynomial_degree()
+            if obj_degree is None or obj_degree > 2:
+                raise DegreeError('CPLEXDirect does not support expressions of degree {0}.'\
+                                  .format(obj_degree))
+            elif obj_degree == 2:
+                quadratic_objective = True
+            else:
+                quadratic_objective = False
+            
+            num_integer_vars = self._solver_model.variables.get_num_integer()
+            num_binary_vars = self._solver_model.variables.get_num_binary()
+            num_sos = self._solver_model.SOS.get_num()
+            
+            if self._solver_model.quadratic_constraints.get_num() != 0:
+                quadratic_cons = True
             else:
-                self._solver_model.set_problem_type(self._solver_model.problem_type.LP)
-
-        for key, option in self.options.items():
-            opt_cmd = self._solver_model.parameters
-            key_pieces = key.split('_')
-            for key_piece in key_pieces:
-                opt_cmd = getattr(opt_cmd, key_piece)
-            # When options come from the pyomo command, all
-            # values are string types, so we try to cast
-            # them to a numeric value in the event that
-            # setting the parameter fails.
-            try:
-                opt_cmd.set(option)
-            except self._cplex.exceptions.CplexError:
-                # we place the exception handling for
-                # checking the cast of option to a float in
-                # another function so that we can simply
-                # call raise here instead of except
-                # TypeError as e / raise e, because the
-                # latter does not preserve the Cplex stack
-                # trace
-                if not _is_numeric(option):
-                    raise
-                opt_cmd.set(float(option))
-
-        t0 = time.time()
-        self._solver_model.solve()
-        t1 = time.time()
-        self._wallclock_time = t1 - t0
+                quadratic_cons = False
+            
+            if (num_integer_vars + num_binary_vars + num_sos) > 0:
+                integer = True
+            else:
+                integer = False
+            
+            if integer:
+                if quadratic_cons:
+                    self._solver_model.set_problem_type(self._solver_model.problem_type.MIQCP)
+                elif quadratic_objective:
+                    self._solver_model.set_problem_type(self._solver_model.problem_type.MIQP)
+                else:
+                    self._solver_model.set_problem_type(self._solver_model.problem_type.MILP)
+            else:
+                if quadratic_cons:
+                    self._solver_model.set_problem_type(self._solver_model.problem_type.QCP)
+                elif quadratic_objective:
+                    self._solver_model.set_problem_type(self._solver_model.problem_type.QP)
+                else:
+                    self._solver_model.set_problem_type(self._solver_model.problem_type.LP)
+            
+            for key, option in self.options.items():
+                opt_cmd = self._solver_model.parameters
+                key_pieces = key.split('_')
+                for key_piece in key_pieces:
+                    opt_cmd = getattr(opt_cmd, key_piece)
+                # When options come from the pyomo command, all
+                # values are string types, so we try to cast
+                # them to a numeric value in the event that
+                # setting the parameter fails.
+                try:
+                    opt_cmd.set(option)
+                except self._cplex.exceptions.CplexError:
+                    # we place the exception handling for
+                    # checking the cast of option to a float in
+                    # another function so that we can simply
+                    # call raise here instead of except
+                    # TypeError as e / raise e, because the
+                    # latter does not preserve the Cplex stack
+                    # trace
+                    if not _is_numeric(option):
+                        raise
+                    opt_cmd.set(float(option))
+            
+            t0 = time.time()
+            self._solver_model.solve()
+            t1 = time.time()
+            self._wallclock_time = t1 - t0
+        finally:
+            if self.version() >= (12, 10):
+                _log_file.close()
 
         # FIXME: can we get a return code indicating if CPLEX had a significant failure?
         return Bunch(rc=None, log=None)


Test patch:
diff --git a/.github/workflows/mac_python_matrix_test.yml b/.github/workflows/mac_python_matrix_test.yml
--- a/.github/workflows/mac_python_matrix_test.yml
+++ b/.github/workflows/mac_python_matrix_test.yml
@@ -4,7 +4,6 @@ on:
   pull_request:
     branches:
       - master
-      # Can add additional branches if desired
 
 jobs:
   pyomo-mac-tests:
@@ -13,11 +12,11 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: [3.5, 3.6, 3.7, 3.8] # All available Python versions
+        python-version: [3.5, 3.6, 3.7, 3.8]
 
     steps:
-    - uses: actions/checkout@v1 # Checkout branch(es)
-    - name: Set up Python ${{ matrix.python-version }} # Initialize Python version
+    - uses: actions/checkout@v1
+    - name: Set up Python ${{ matrix.python-version }}
       uses: actions/setup-python@v1
       with:
         python-version: ${{ matrix.python-version }}
@@ -30,18 +29,23 @@ jobs:
         brew install pkg-config
         brew install unixodbc
         brew install freetds # Now install Python modules
+        echo "Upgrade pip..."
         python -m pip install --upgrade pip
         echo ""
         echo "Install Pyomo dependencies..."
         echo ""
         pip install cython numpy scipy ipython openpyxl sympy pyyaml pyodbc networkx xlrd pandas matplotlib dill seaborn pymysql pyro4 pint pathos
         echo ""
+        echo "Install CPLEX Community Edition..."
+        echo ""
+        pip install cplex || echo "CPLEX Community Edition is not available for ${{ matrix.python-version }}"
+        echo ""
         echo "Install GAMS..."
         echo ""
-        wget -q https://d37drm4t2jghv5.cloudfront.net/distributions/29.1.0/macosx/osx_x64_64_sfx.exe
-        chmod +x osx_x64_64_sfx.exe
-        ./osx_x64_64_sfx.exe -q -d gams
-        cd gams/gams29.1_osx_x64_64_sfx/apifiles/Python/
+        wget -q https://d37drm4t2jghv5.cloudfront.net/distributions/29.1.0/macosx/osx_x64_64_sfx.exe -O gams_installer.exe
+        chmod +x gams_installer.exe
+        ./gams_installer.exe -q -d gams
+        cd gams/*/apifiles/Python/
         py_ver=$(python -c 'import sys;print("%s%s" % sys.version_info[:2])')
         gams_ver=api
         for ver in api_*; do
@@ -62,17 +66,17 @@ jobs:
         echo ""
         echo "Install Pyomo..."
         echo ""
-        python setup.py develop # Install Pyomo
+        python setup.py develop
         echo ""
-        echo "Install extensions..."
+        echo "Download and install extensions..."
         echo ""
-        pyomo download-extensions # Get Pyomo extensions
+        pyomo download-extensions
         pyomo build-extensions
-    - name: Run nightly, not fragile tests with test.pyomo
+    - name: Run nightly tests with test.pyomo
       run: |
         echo "Run test.pyomo..."
-        export PATH=$PATH:$(pwd)/gams/gams29.1_osx_x64_64_sfx/ 
-        export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$(pwd)/gams/gams29.1_osx_x64_64_sfx/
+        GAMS_DIR=`ls -d1 $(pwd)/gams/*/ | head -1`
+        export PATH=$PATH:$GAMS_DIR
+        export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$GAMS_DIR
         pip install nose
-        KEY_JOB=1
-        test.pyomo -v --cat="nightly" pyomo `pwd`/pyomo-model-libraries # Run nightly, stable tests
+        test.pyomo -v --cat="nightly" pyomo `pwd`/pyomo-model-libraries
diff --git a/.github/workflows/ubuntu_python_matrix_test.yml b/.github/workflows/ubuntu_python_matrix_test.yml
--- a/.github/workflows/ubuntu_python_matrix_test.yml
+++ b/.github/workflows/ubuntu_python_matrix_test.yml
@@ -13,6 +13,7 @@ jobs:
       fail-fast: false
       matrix:
         python-version: [3.5, 3.6, 3.7, 3.8]
+
     steps:
     - uses: actions/checkout@v1
     - name: Set up Python ${{ matrix.python-version }}
@@ -24,16 +25,20 @@ jobs:
         echo "Upgrade pip..."
         python -m pip install --upgrade pip
         echo ""
-        echo "Install extras..."
+        echo "Install Pyomo dependencies..."
+        echo ""
+        pip install cython numpy scipy ipython openpyxl sympy pyyaml pyodbc networkx xlrd pandas matplotlib dill seaborn pymysql pyro4 pint pathos
+        echo ""
+        echo "Install CPLEX Community Edition..."
         echo ""
-        pip install numpy scipy ipython openpyxl sympy pyodbc pyyaml networkx xlrd matplotlib dill pandas seaborn pymysql pyro4 pint pathos 
+        pip install cplex || echo "CPLEX Community Edition is not available for ${{ matrix.python-version }}"
         echo ""
         echo "Install GAMS..."
         echo ""
-        wget -q https://d37drm4t2jghv5.cloudfront.net/distributions/29.1.0/linux/linux_x64_64_sfx.exe
-        chmod +x linux_x64_64_sfx.exe
-        ./linux_x64_64_sfx.exe -q -d gams
-        cd gams/gams29.1_linux_x64_64_sfx/apifiles/Python/
+        wget -q https://d37drm4t2jghv5.cloudfront.net/distributions/29.1.0/linux/linux_x64_64_sfx.exe -O gams_installer.exe
+        chmod +x gams_installer.exe
+        ./gams_installer.exe -q -d gams
+        cd gams/*/apifiles/Python/
         py_ver=$(python -c 'import sys;print("%s%s" % sys.version_info[:2])')
         gams_ver=api
         for ver in api_*; do
@@ -63,8 +68,8 @@ jobs:
     - name: Run nightly tests with test.pyomo
       run: |
         echo "Run test.pyomo..."
-        export PATH=$PATH:$(pwd)/gams/gams29.1_linux_x64_64_sfx/
-        export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(pwd)/gams/gams29.1_linux_x64_64_sfx/
+        GAMS_DIR=`ls -d1 $(pwd)/gams/*/ | head -1`
+        export PATH=$PATH:$GAMS_DIR
+        export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$GAMS_DIR
         pip install nose
-        KEY_JOB=1
-        test.pyomo -v --cat="nightly" pyomo `pwd`/pyomo-model-libraries
\ No newline at end of file
+        test.pyomo -v --cat="nightly" pyomo `pwd`/pyomo-model-libraries
diff --git a/.github/workflows/win_python_matrix_test.yml b/.github/workflows/win_python_matrix_test.yml
--- a/.github/workflows/win_python_matrix_test.yml
+++ b/.github/workflows/win_python_matrix_test.yml
@@ -47,6 +47,22 @@ jobs:
         $env:ADDITIONAL_CF_PKGS=$env:ADDITIONAL_CF_PKGS + "pymysql pyro4 pint pathos " + $env:MINICONDA_EXTRAS
         $env:ADDITIONAL_CF_PKGS=$env:ADDITIONAL_CF_PKGS + " glpk ipopt"
         $env:EXP = $env:CONDAFORGE + $env:ADDITIONAL_CF_PKGS
+        $env:CPLEX = $env:CONDAFORGE + "-c ibmdecisionoptimization cplex=12.10"
+        Write-Host ("")
+        Write-Host ("Try to install CPLEX...")
+        Write-Host ("")
+        try
+        {
+            Invoke-Expression $env:CPLEX
+        }
+        catch
+        {
+            Write-Host ("##########################################################################")
+            Write-Host ("WARNING: CPLEX Community Edition is not available for Python ${{ matrix.python-version }}")
+            Write-Host ("##########################################################################")
+            conda deactivate
+            conda activate test
+        }
         Invoke-Expression $env:EXP
         $env:PYNUMERO = $env:CONDAFORGE + " pynumero_libraries"
         Write-Host ("")