diff --git a/.github/actions/setup-idaes/action.yml b/.github/actions/setup-idaes/action.yml index c310078609..61bab7c906 100644 --- a/.github/actions/setup-idaes/action.yml +++ b/.github/actions/setup-idaes/action.yml @@ -42,3 +42,9 @@ runs: echo '::group::Output of "idaes get-extensions" command' idaes get-extensions --extra petsc --verbose echo '::endgroup::' + - name: Install SCIP from AMPL + shell: bash -l {0} + run: | + echo '::group::Output of "pip install ampl_module_scip" command' + ${{ inputs.install-command }} --index-url https://pypi.ampl.com ampl_module_scip + echo '::endgroup::' diff --git a/idaes/core/util/testing.py b/idaes/core/util/testing.py index 259df64da0..11be9aaf9b 100644 --- a/idaes/core/util/testing.py +++ b/idaes/core/util/testing.py @@ -21,8 +21,13 @@ __author__ = "Andrew Lee" +import os +from typing import Callable, Union + from pyomo.environ import Constraint, Set, units, Var from pyomo.common.config import ConfigBlock +from pyomo.common import Executable +from pyomo.common.dependencies import attempt_import from idaes.core import ( declare_process_block_class, @@ -409,3 +414,36 @@ def get_reaction_rate_basis(b): return MaterialFlowBasis.mass else: return MaterialFlowBasis.other + + +def _enable_scip_solver_for_testing( + name: str = "scip", +) -> Union[Callable[[], None], None]: + ampl_module_scip, is_available = attempt_import("ampl_module_scip") + if not is_available: + _log.warning( + "ampl_module_scip must be installed to enable SCIP solver for testing" + ) + return None + + new_path_entry = ampl_module_scip.bin_dir + # TODO prepending new_path_entry to PATH means that the "testing" SCIP will "shadow" + # existing executable directories already on PATH + # this behavior would match the (implied) semantics of this function, i.e. + # "enable SCIP solver used for testing (at the expense of other non-testing SCIP executables)" + os.environ["PATH"] = os.pathsep.join([new_path_entry, os.environ["PATH"]]) + Executable.rehash() + + def remove_from_path(): + path_as_list = os.environ["PATH"].split(os.pathsep) + + try: + path_as_list.remove(new_path_entry) + except ValueError: + # not in PATH + pass + else: + os.environ["PATH"] = os.pathsep.join(path_as_list) + Executable.rehash() + + return remove_from_path diff --git a/idaes/core/util/tests/test_model_diagnostics.py b/idaes/core/util/tests/test_model_diagnostics.py index fe59206ad9..de398d5374 100644 --- a/idaes/core/util/tests/test_model_diagnostics.py +++ b/idaes/core/util/tests/test_model_diagnostics.py @@ -77,10 +77,24 @@ _write_report_section, _collect_model_statistics, ) +from idaes.core.util.testing import _enable_scip_solver_for_testing + __author__ = "Alex Dowling, Douglas Allan, Andrew Lee" -solver_available = SolverFactory("scip").available() + +@pytest.fixture(scope="module") +def scip_solver(): + solver = SolverFactory("scip") + undo_changes = None + + if not solver.available(): + undo_changes = _enable_scip_solver_for_testing() + if not solver.available(): + pytest.skip(reason="SCIP solver not available") + yield solver + if undo_changes is not None: + undo_changes() @pytest.fixture @@ -1790,27 +1804,26 @@ def test_identify_candidates(self, model): @pytest.mark.solver @pytest.mark.component - @pytest.mark.skipif(not solver_available, reason="SCIP is not available") - def test_solve_candidates_milp(self, model): + def test_solve_candidates_milp(self, model, scip_solver): dh = DegeneracyHunter2(model) dh._prepare_candidates_milp() dh._solve_candidates_milp() - assert value(dh.candidates_milp.nu[0]) == pytest.approx(-1e-05, rel=1e-5) - assert value(dh.candidates_milp.nu[1]) == pytest.approx(1e-05, rel=1e-5) + assert value(dh.candidates_milp.nu[0]) == pytest.approx(1e-05, rel=1e-5) + assert value(dh.candidates_milp.nu[1]) == pytest.approx(-1e-05, rel=1e-5) assert value(dh.candidates_milp.y_pos[0]) == pytest.approx(0, abs=1e-5) - assert value(dh.candidates_milp.y_pos[1]) == pytest.approx(1, rel=1e-5) + assert value(dh.candidates_milp.y_pos[1]) == pytest.approx(0, rel=1e-5) - assert value(dh.candidates_milp.y_neg[0]) == pytest.approx(-0, abs=1e-5) - assert value(dh.candidates_milp.y_neg[1]) == pytest.approx(-0, abs=1e-5) + assert value(dh.candidates_milp.y_neg[0]) == pytest.approx(0, abs=1e-5) + assert value(dh.candidates_milp.y_neg[1]) == pytest.approx(1, abs=1e-5) assert value(dh.candidates_milp.abs_nu[0]) == pytest.approx(1e-05, rel=1e-5) assert value(dh.candidates_milp.abs_nu[1]) == pytest.approx(1e-05, rel=1e-5) assert dh.degenerate_set == { - model.con2: -1e-05, - model.con5: 1e-05, + model.con2: value(dh.candidates_milp.nu[0]), + model.con5: value(dh.candidates_milp.nu[1]), } @pytest.mark.unit @@ -1840,8 +1853,7 @@ def test_solve_ids_milp(self, model): @pytest.mark.solver @pytest.mark.component - @pytest.mark.skipif(not solver_available, reason="SCIP is not available") - def test_solve_ids_milp(self, model): + def test_solve_ids_milp(self, model, scip_solver): dh = DegeneracyHunter2(model) dh._prepare_ids_milp() ids_ = dh._solve_ids_milp(cons=model.con2) @@ -1859,8 +1871,7 @@ def test_solve_ids_milp(self, model): @pytest.mark.solver @pytest.mark.component - @pytest.mark.skipif(not solver_available, reason="SCIP is not available") - def test_find_irreducible_degenerate_sets(self, model): + def test_find_irreducible_degenerate_sets(self, model, scip_solver): dh = DegeneracyHunter2(model) dh.find_irreducible_degenerate_sets() @@ -1871,8 +1882,7 @@ def test_find_irreducible_degenerate_sets(self, model): @pytest.mark.solver @pytest.mark.component - @pytest.mark.skipif(not solver_available, reason="SCIP is not available") - def test_report_irreducible_degenerate_sets(self, model): + def test_report_irreducible_degenerate_sets(self, model, scip_solver): stream = StringIO() dh = DegeneracyHunter2(model) @@ -1898,8 +1908,7 @@ def test_report_irreducible_degenerate_sets(self, model): @pytest.mark.solver @pytest.mark.component - @pytest.mark.skipif(not solver_available, reason="SCIP is not available") - def test_report_irreducible_degenerate_sets_none(self, model): + def test_report_irreducible_degenerate_sets_none(self, model, scip_solver): stream = StringIO() # Delete degenerate constraint diff --git a/idaes/core/util/tests/test_scip_for_testing.py b/idaes/core/util/tests/test_scip_for_testing.py new file mode 100644 index 0000000000..baa59878a3 --- /dev/null +++ b/idaes/core/util/tests/test_scip_for_testing.py @@ -0,0 +1,39 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +import os + +import pytest +from pyomo.environ import SolverFactory + +from idaes.core.util.testing import _enable_scip_solver_for_testing + + +ampl_module_scip = pytest.importorskip( + "ampl_module_scip", reason="'ampl_module_scip' not available" +) + + +@pytest.mark.unit +def test_path_manipulation(): + path_before_enabling = str(os.environ["PATH"]) + + func_to_revert_changes = _enable_scip_solver_for_testing() + path_after_enabling = str(os.environ["PATH"]) + sf = SolverFactory("scip") + assert len(path_after_enabling) > len(path_before_enabling) + assert str(ampl_module_scip.bin_dir) in str(sf.executable()) + assert sf.available() + + func_to_revert_changes() + path_after_reverting = str(os.environ["PATH"]) + assert path_after_reverting == path_before_enabling diff --git a/setup.py b/setup.py index c9680b75f7..6887335c28 100644 --- a/setup.py +++ b/setup.py @@ -127,7 +127,7 @@ def __getitem__(self, key): # Put abstract (non-versioned) deps here. # Concrete dependencies go in requirements[-dev].txt install_requires=[ - "pyomo>=6.6.2", + "pyomo @ git+https://github.com/IDAES/Pyomo@6.7.0.idaes.2023.11.6", "pint", # required to use Pyomo units "networkx", # required to use Pyomo network "numpy",