From 5630263d37613331281af445551b0b0f6ce791ff Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Thu, 29 Feb 2024 18:30:03 -0800 Subject: [PATCH 01/30] getting started moving mis code into Pyomo contrib --- doc/OnlineDocs/contributed_packages/index.rst | 1 + doc/OnlineDocs/contributed_packages/mis.rst | 114 ++++++ pyomo/contrib/mis/__init__.py | 12 + pyomo/contrib/mis/mis.py | 376 ++++++++++++++++++ 4 files changed, 503 insertions(+) create mode 100644 doc/OnlineDocs/contributed_packages/mis.rst create mode 100644 pyomo/contrib/mis/__init__.py create mode 100644 pyomo/contrib/mis/mis.py diff --git a/doc/OnlineDocs/contributed_packages/index.rst b/doc/OnlineDocs/contributed_packages/index.rst index b1d9cbbad3b..33e1c7f851d 100644 --- a/doc/OnlineDocs/contributed_packages/index.rst +++ b/doc/OnlineDocs/contributed_packages/index.rst @@ -30,6 +30,7 @@ Contributed packages distributed with Pyomo: pyros.rst sensitivity_toolbox.rst trustregion.rst + mis.rst Contributed Pyomo interfaces to other packages: diff --git a/doc/OnlineDocs/contributed_packages/mis.rst b/doc/OnlineDocs/contributed_packages/mis.rst new file mode 100644 index 00000000000..84f06aba541 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mis.rst @@ -0,0 +1,114 @@ +Minimal Intractable System finder +================================= + +The file ``mis.py`` finds sets of actions that each, independently, +would result in feasibility. The zero-tolerance is whatever the +solver uses, so users may want to post-process output if it is going +to be used for analysis. It also computes a minimal intractable system +(which is not guaranteed to be unique). It was written by Ben Knueven +as part of the watertap project and is governed by a license shown +at the bottom of the ``mis.py``. + +The algortithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf + +Solver +------ + +At the time of this writing, you need to use ipopt even for LPs. + +Quick Start +----------- + +The file ``trivial_mis.py`` is a tiny example listed at the bottom of +this help file, which references a pyomo model with the Python variable +`m` and has these lines: + +.. code-block:: python + + from mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) + +.. Note:: + This is done instead of solving the problem. + +.. Note:: + If IDAES is installed, the `solver` keyward argument + is not needed (the function will use IDAES to find + a solver). + +Interpreting the Output +----------------------- + +Assuming the dependencies are installed ``trivial_mis.py`` will +produce a lot of warnings from ipopt and then meaninful output: + +Repair Options +^^^^^^^^^^^^^^ + +This output shows three independent ways that the model could be rendered feasible. + + +.. raw:: + + Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: + ub of var x[1] by 4.464126126706818e-05 + lb of var x[2] by 0.9999553410114216 + Another feasible solution was found with only the following variable bounds relaxed: + lb of var x[1] by 0.7071067726864677 + ub of var x[2] by 0.41421355687130673 + ub of var y by 0.7071067651855212 + Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: + constraint: c by 0.9999999861866736 + + +Minimal Intractable System (MIS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This output shows a minimal intractable system: + + +.. raw:: + + Computed Minimal Intractable System (MIS)! + Constraints / bounds in MIS: + lb of var x[2] + lb of var x[1] + constraint: c + +Constraints / bounds in guards for stability +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This part of the report for nonlinear programs (NLPs). + +When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver +to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` +object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the +set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, +just that “bad things” happen when they’re not included. + +Perhpas ideally we would put a constraint in the “guard” set if Ipopt failed to converge, and only put it in the +MIS if Ipopt converged to a point of local infeasibility. However, right now the code generally makes the +assumption that if Ipopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. +Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the +constraints in the elastic filter may be feasible -- because Ipopt failed to converge and we assumed that meant the +subproblem was not feasible. + +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when it’s +assumptions are not satisfied. + +trivial_mis.py +-------------- + +.. code-block:: python + + import pyomo.environ as pyo + m = pyo.ConcreteModel("Trivial Quad") + m.x = pyo.Var([1,2], bounds=(0,1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + from mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) diff --git a/pyomo/contrib/mis/__init__.py b/pyomo/contrib/mis/__init__.py new file mode 100644 index 00000000000..7375e97b503 --- /dev/null +++ b/pyomo/contrib/mis/__init__.py @@ -0,0 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.contrib.iis.iis import compute_infeasibility_explanation diff --git a/pyomo/contrib/mis/mis.py b/pyomo/contrib/mis/mis.py new file mode 100644 index 00000000000..acf443f321a --- /dev/null +++ b/pyomo/contrib/mis/mis.py @@ -0,0 +1,376 @@ +""" +Minimal Intractable System (MIS) finder +Originall written by Ben Knueven as part of the WaterTAP project: + https://github.com/watertap-org/watertap +That's why DLW put a huge license notice at the bottom of this file. + +copied by DLW 18Feb2024 and edited + +See: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf +""" +import pyomo.environ as pyo + +from pyomo.core.plugins.transform.add_slack_vars import AddSlackVariables + +from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation + +from pyomo.common.modeling import unique_component_name +from pyomo.common.collections import ComponentMap, ComponentSet + +from pyomo.opt import WriterFactory + +try: + from idaes.core.solvers import get_solver + have_idaes = True +except: + have_idaes = False + +logger = logging.getLogger("pyomo.contrib.iis") +logger.setLevel(logging.INFO) + +_default_nl_writer = WriterFactory.get_class("nl") + + +class _VariableBoundsAsConstraints(IsomorphicTransformation): + """Replace all variables bounds and domain information with constraints. + + Leaves fixed Vars untouched (for now) + """ + + def _apply_to(self, instance, **kwds): + + boundconstrblockname = unique_component_name(instance, "_variable_bounds") + instance.add_component(boundconstrblockname, pyo.Block()) + boundconstrblock = instance.component(boundconstrblockname) + + for v in instance.component_data_objects(pyo.Var, descend_into=True): + if v.fixed: + continue + lb, ub = v.bounds + if lb is None and ub is None: + continue + var_name = v.getname(fully_qualified=True) + if lb is not None: + con_name = "lb_for_" + var_name + con = pyo.Constraint(expr=(lb, v, None)) + boundconstrblock.add_component(con_name, con) + if ub is not None: + con_name = "ub_for_" + var_name + con = pyo.Constraint(expr=(None, v, ub)) + boundconstrblock.add_component(con_name, con) + + # now we deactivate the variable bounds / domain + v.domain = pyo.Reals + v.setlb(None) + v.setub(None) + + +def compute_infeasibility_explanation(model, solver=None, tee=False, tolerance=1e-8, logger=logger): + """ + This function attempts to determine why a given model is infeasible. It deploys + two main algorithms: + + 1. Successfully relaxes the constraints of the problem, and reports to the user + some sets of constraints and variable bounds, which when relaxed, creates a + feasible model. + 2. Uses the information collected from (1) to attempt to compute a Minimal + Infeasible System (MIS), which is a set of constraints and variable bounds + which appear to be in conflict with each other. It is minimal in the sense + that removing any single constraint or variable bound would result in a + feasible subsystem. + + Args + ---- + model: A pyomo block + solver (optional): A pyomo solver, a string, or None + tee (optional): Display intermediate solves conducted (False) + tolerance (optional): The feasibility tolerance to use when declaring a + constraint feasible (1e-08) + logger:logging.Logger + A logger for messages. Uses pyomo.contrib.mis logger by default. + + """ + + # hold the original harmless + modified_model = model.clone() + + if solver is None: + if have_idaes: + solver = get_solver() + else: + raise ValueError("solver needed unless IDAES is installed") + elif isinstance(solver, str): + solver = pyo.SolverFactory(solver) + else: + # assume we have a solver + assert solver.available() + + # first, cache the values we get + _value_cache = ComponentMap() + for v in model.component_data_objects(pyo.Var, descend_into=True): + _value_cache[v] = v.value + + # finding proper reference + if model.parent_block() is None: + common_name = "" + else: + common_name = model.name + "." + + _modified_model_var_to_original_model_var = ComponentMap() + _modified_model_value_cache = ComponentMap() + + for v in model.component_data_objects(pyo.Var, descend_into=True): + modified_model_var = modified_model.find_component(v.name[len(common_name) :]) + + _modified_model_var_to_original_model_var[modified_model_var] = v + _modified_model_value_cache[modified_model_var] = _value_cache[v] + modified_model_var.set_value(_value_cache[v], skip_validation=True) + + # TODO: For WT / IDAES models, we should probably be more + # selective in *what* we elasticize. E.g., it probably + # does not make sense to elasticize property calculations + # and maybe certain other equality constraints calculating + # values. Maybe we shouldn't elasticize *any* equality + # constraints. + # For example, elasticizing the calculation of mass fraction + # makes absolutely no sense and will just be noise for the + # modeler to sift through. We could try to sort the constraints + # such that we look for those with linear coefficients `1` on + # some term and leave those be. + # move the variable bounds to the constraints + _VariableBoundsAsConstraints().apply_to(modified_model) + + AddSlackVariables().apply_to(modified_model) + slack_block = modified_model._core_add_slack_variables + + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + # start with variable bounds -- these are the easist to interpret + for c in modified_model._variable_bounds.component_data_objects( + pyo.Constraint, descend_into=True + ): + plus = slack_block.component(f"_slack_plus_{c.name}") + minus = slack_block.component(f"_slack_minus_{c.name}") + assert not (plus is None and minus is None) + if plus is not None: + plus.unfix() + if minus is not None: + minus.unfix() + + # TODO: Elasticizing too much at once seems to cause Ipopt trouble. + # After an initial sweep, we should just fix one elastic variable + # and put everything else on a stack of "constraints to elasticize". + # We elastisize one constraint at a time and fix one constraint at a time. + # After fixing an elastic variable, we elasticize a single constraint it + # appears in and put the remaining constraints on the stack. If the resulting problem + # is feasible, we keep going "down the tree". If the resulting problem is + # infeasible or cannot be solved, we elasticize a single constraint from + # the top of the stack. + # The algorithm stops when the stack is empty and the subproblem is infeasible. + # Along the way, any time the current problem is infeasible we can check to + # see if the current set of constraints in the filter is as a collection of + # infeasible constraints -- to terminate early. + # However, while more stable, this is much more computationally intensive. + # So, we leave the implementation simpler for now and consider this as + # a potential extension if this tool sometimes cannot report a good answer. + # Phase 1 -- build the initial set of constraints, or prove feasibility + msg = "" + fixed_slacks = ComponentSet() + elastic_filter = ComponentSet() + + def _constraint_loop(relaxed_things, msg): + if msg == "": + msg += f"Model {model.name} may be infeasible. A feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + while True: + + def _constraint_generator(): + elastic_filter_size_initial = len(elastic_filter) + for v in slack_block.component_data_objects(pyo.Var): + if v.value > tolerance: + constr = _get_constraint(modified_model, v) + yield constr, v.value + v.fix(0) + fixed_slacks.add(v) + elastic_filter.add(constr) + if len(elastic_filter) == elastic_filter_size_initial: + raise Exception(f"Found model {model.name} to be feasible!") + + msg = _get_results_with_value(_constraint_generator(), msg) + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + break + return msg + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("variable bounds", msg) + + # next, try relaxing the inequality constraints + for v in slack_block.component_data_objects(pyo.Var): + c = _get_constraint(modified_model, v) + if c.equality: + # equality constraint + continue + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("inequality constraints and/or variable bounds", msg) + + for v in slack_block.component_data_objects(pyo.Var): + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop( + "inequality constraints, equality constraints, and/or variable bounds", msg + ) + + if len(elastic_filter) == 0: + # load the feasible solution into the original model + for modified_model_var, v in _modified_model_var_to_original_model_var.items(): + v.set_value(modified_model_var.value, skip_validation=True) + results = solver.solve(model, tee=tee) + if pyo.check_optimal_termination(results): + logger.info(f"A feasible solution was found!") + else: + logger.info( + f"Could not find a feasible solution with violated constraints or bounds. This model is likely unstable" + ) + + # Phase 2 -- deletion filter + # TODO: the model created here seems to mess with the nl_v2 + # writer sometimes. So we temporarily switch to nl_v1 writer. + WriterFactory.register("nl")(WriterFactory.get_class("nl_v1")) + + # remove slacks by fixing them to 0 + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + for o in modified_model.component_data_objects(pyo.Objective, descend_into=True): + o.deactivate() + + # mark all constraints not in the filter as inactive + for c in modified_model.component_data_objects(pyo.Constraint): + if c in elastic_filter: + continue + else: + c.deactivate() + + try: + results = solver.solve(modified_model, tee=tee) + except: + results = None + + if pyo.check_optimal_termination(results): + msg += "Could not determine Minimal Intractable System\n" + else: + deletion_filter = [] + guards = [] + for constr in elastic_filter: + constr.deactivate() + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + try: + results = solver.solve(modified_model, tee=tee) + except: + math_failure = True + else: + math_failure = False + + if math_failure: + constr.activate() + guards.append(constr) + elif pyo.check_optimal_termination(results): + constr.activate() + deletion_filter.append(constr) + else: # still infeasible without this constraint + pass + + msg += "Computed Minimal Intractable System (MIS)!\n" + msg += "Constraints / bounds in MIS:\n" + msg = _get_results(deletion_filter, msg) + msg += "Constraints / bounds in guards for stability:" + msg = _get_results(guards, msg) + + WriterFactory.register("nl")(_default_nl_writer) + + logger.info(msg) + + +def _get_results_with_value(constr_value_generator, msg=None): + if msg is None: + msg = "" + for c, value in constr_value_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]} by {value}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]} by {value}\n" + else: + raise RuntimeError("unrecongized var name") + else: + msg += f"\tconstraint: {c_name} by {value}\n" + return msg + + +def _get_results(constr_generator, msg=None): + if msg is None: + msg = "" + for c in constr_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]}\n" + else: + raise RuntimeError("unrecongized var name") + else: + msg += f"\tconstraint: {c_name}\n" + return msg + + +def _get_constraint(modified_model, v): + if "_slack_plus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_plus_") :]) + if constr is None: + raise RuntimeError( + "Bad constraint name {v.local_name[len('_slack_plus_'):]}" + ) + return constr + elif "_slack_minus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_minus_") :]) + if constr is None: + raise RuntimeError( + "Bad constraint name {v.local_name[len('_slack_minus_'):]}" + ) + return constr + else: + raise RuntimeError("Bad var name {v.name}") + +""" +WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, and National Energy Technology Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + Neither the name of the University of California, Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, National Energy Technology Laboratory, U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, without imposing a separate written license agreement for such Enhancements, then you hereby grant the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare derivative works, incorporate into other computer software, distribute, and sublicense such enhancements or derivative works thereof, in binary and source code form. +""" From bceaff1473b7dc44fff38fa3715d234d4c0a3079 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Thu, 29 Feb 2024 19:24:06 -0800 Subject: [PATCH 02/30] we have a test for mis, but it needs more coverage --- doc/OnlineDocs/contributed_packages/mis.rst | 13 ++-- pyomo/contrib/mis/__init__.py | 2 +- pyomo/contrib/mis/mis.py | 1 + pyomo/contrib/mis/tests/test_mis.py | 85 +++++++++++++++++++++ pyomo/contrib/mis/tests/trivial_mis.py | 14 ++++ 5 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 pyomo/contrib/mis/tests/test_mis.py create mode 100644 pyomo/contrib/mis/tests/trivial_mis.py diff --git a/doc/OnlineDocs/contributed_packages/mis.rst b/doc/OnlineDocs/contributed_packages/mis.rst index 84f06aba541..9e92812b5fe 100644 --- a/doc/OnlineDocs/contributed_packages/mis.rst +++ b/doc/OnlineDocs/contributed_packages/mis.rst @@ -25,7 +25,7 @@ this help file, which references a pyomo model with the Python variable .. code-block:: python - from mis import compute_infeasibility_explanation + from pyomo.contrib.mis import compute_infeasibility_explanation ipopt = pyo.SolverFactory("ipopt") compute_infeasibility_explanation(m, solver=ipopt) @@ -40,13 +40,14 @@ this help file, which references a pyomo model with the Python variable Interpreting the Output ----------------------- -Assuming the dependencies are installed ``trivial_mis.py`` will -produce a lot of warnings from ipopt and then meaninful output: +Assuming the dependencies are installed, file ``trivial_mis.py`` +(shown below) will +produce a lot of warnings from ipopt and then meaninful output in a log file. Repair Options ^^^^^^^^^^^^^^ -This output shows three independent ways that the model could be rendered feasible. +This output for the trivial example shows three independent ways that the model could be rendered feasible: .. raw:: @@ -79,7 +80,7 @@ This output shows a minimal intractable system: Constraints / bounds in guards for stability ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This part of the report for nonlinear programs (NLPs). +This part of the report is for nonlinear programs (NLPs). When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` @@ -109,6 +110,6 @@ trivial_mis.py m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) - from mis import compute_infeasibility_explanation + from pyomo.contrib.mis import compute_infeasibility_explanation ipopt = pyo.SolverFactory("ipopt") compute_infeasibility_explanation(m, solver=ipopt) diff --git a/pyomo/contrib/mis/__init__.py b/pyomo/contrib/mis/__init__.py index 7375e97b503..0235c769467 100644 --- a/pyomo/contrib/mis/__init__.py +++ b/pyomo/contrib/mis/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.contrib.iis.iis import compute_infeasibility_explanation +from pyomo.contrib.mis.mis import compute_infeasibility_explanation diff --git a/pyomo/contrib/mis/mis.py b/pyomo/contrib/mis/mis.py index acf443f321a..86832f36933 100644 --- a/pyomo/contrib/mis/mis.py +++ b/pyomo/contrib/mis/mis.py @@ -8,6 +8,7 @@ See: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf """ +import logging import pyomo.environ as pyo from pyomo.core.plugins.transform.add_slack_vars import AddSlackVariables diff --git a/pyomo/contrib/mis/tests/test_mis.py b/pyomo/contrib/mis/tests/test_mis.py new file mode 100644 index 00000000000..9fa80f5510b --- /dev/null +++ b/pyomo/contrib/mis/tests/test_mis.py @@ -0,0 +1,85 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import pyomo.environ as pyo +from pyomo.contrib.mis import compute_infeasibility_explanation +from pyomo.common.tempfiles import TempfileManager + +import logging +import os + + +def _get_infeasible_model(): + m = pyo.ConcreteModel() + m.x = pyo.Var(within=pyo.Binary) + m.y = pyo.Var(within=pyo.NonNegativeReals) + + m.c1 = pyo.Constraint(expr=m.y <= 100.0 * m.x) + m.c2 = pyo.Constraint(expr=m.y <= -100.0 * m.x) + m.c3 = pyo.Constraint(expr=m.x >= 0.5) + + m.o = pyo.Objective(expr=-m.y) + + return m + + +class TestMIS(unittest.TestCase): + @unittest.skipUnless( + pyo.SolverFactory("ipopt").available(exception_flag=False), + "ipopt not available", + ) + def test_write_mis_ipopt(self): + _test_mis("ipopt") + +def _check_output(file_name): + # pretty simple check for now + with open(file_name, "r+") as file1: + lines = file1.readlines() + trigger = "Constraints / bounds in MIS:" + nugget = "lb of var y" + live = False # (long i) + wewin = False + for line in lines: + if trigger in line: + live = True + if live: + if nugget in line: + wewin = True + if not wewin: + raise RuntimeError(f"Did not find '{nugget}' after '{trigger}' in output") + else: + pass + + +def _test_mis(solver_name): + m = _get_infeasible_model() + opt = pyo.SolverFactory(solver_name) + + TempfileManager.push() + tmp_path = TempfileManager.create_tempdir() + file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") + logger = logging.getLogger(f'test_mis_{solver_name}') + logger.setLevel(logging.INFO) + # create file handler which logs even debug messages + print(f"{file_name =}") + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + + TempfileManager.pop() + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/mis/tests/trivial_mis.py b/pyomo/contrib/mis/tests/trivial_mis.py new file mode 100644 index 00000000000..37f177c7666 --- /dev/null +++ b/pyomo/contrib/mis/tests/trivial_mis.py @@ -0,0 +1,14 @@ +import pyomo.environ as pyo +m = pyo.ConcreteModel("Trivial Quad") +m.x = pyo.Var([1,2], bounds=(0,1)) +m.y = pyo.Var(bounds=(0, 1)) +m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) +m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + +from pyomo.contrib.mis.mis import compute_infeasibility_explanation +# if IDAES is installed, compute_infeasibility_explanation doesn't need to be passed a solver +# Note: this particular little problem is quadratic +# As of 18Feb DLW is not sure the explanation code works with solvers other than ipopt +ipopt = pyo.SolverFactory("ipopt") +compute_infeasibility_explanation(m, solver=ipopt) + From fc9167b5ed84c05b9bb7674ddbe3b4833cbaed52 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 05:29:27 -0800 Subject: [PATCH 03/30] now testing some exceptions --- pyomo/contrib/mis/__init__.py | 2 ++ pyomo/contrib/mis/tests/test_mis.py | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mis/__init__.py b/pyomo/contrib/mis/__init__.py index 0235c769467..5f652020dfa 100644 --- a/pyomo/contrib/mis/__init__.py +++ b/pyomo/contrib/mis/__init__.py @@ -10,3 +10,5 @@ # ___________________________________________________________________________ from pyomo.contrib.mis.mis import compute_infeasibility_explanation +# so the tests can find it +from pyomo.contrib.mis.mis import _get_constraint diff --git a/pyomo/contrib/mis/tests/test_mis.py b/pyomo/contrib/mis/tests/test_mis.py index 9fa80f5510b..6776f43f4ce 100644 --- a/pyomo/contrib/mis/tests/test_mis.py +++ b/pyomo/contrib/mis/tests/test_mis.py @@ -11,7 +11,7 @@ import pyomo.common.unittest as unittest import pyomo.environ as pyo -from pyomo.contrib.mis import compute_infeasibility_explanation +import pyomo.contrib.mis as mis from pyomo.common.tempfiles import TempfileManager import logging @@ -39,7 +39,20 @@ class TestMIS(unittest.TestCase): ) def test_write_mis_ipopt(self): _test_mis("ipopt") + def test__get_constraint_errors(self): + # A not-completely-cyincal way to get the coverage up. + m = _get_infeasible_model() # not modified, but who cares? + fct = getattr(mis, "_get_constraint") + #fct = mis._get_constraint + m.foo_slack_plus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_plus_) + m.foo_slack_minus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_minus_) + m.foo_bar = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_bar) + + def _check_output(file_name): # pretty simple check for now with open(file_name, "r+") as file1: @@ -75,7 +88,7 @@ def _test_mis(solver_name): fh.setLevel(logging.DEBUG) logger.addHandler(fh) - compute_infeasibility_explanation(m, opt, logger=logger) + mis.compute_infeasibility_explanation(m, opt, logger=logger) _check_output(file_name) TempfileManager.pop() From 3030fdc019e878081aa98e6bfa11036d7fd5b7c5 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 09:35:43 -0800 Subject: [PATCH 04/30] slight change to doc --- doc/OnlineDocs/contributed_packages/mis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/mis.rst b/doc/OnlineDocs/contributed_packages/mis.rst index 9e92812b5fe..02117867dd5 100644 --- a/doc/OnlineDocs/contributed_packages/mis.rst +++ b/doc/OnlineDocs/contributed_packages/mis.rst @@ -42,7 +42,7 @@ Interpreting the Output Assuming the dependencies are installed, file ``trivial_mis.py`` (shown below) will -produce a lot of warnings from ipopt and then meaninful output in a log file. +produce a lot of warnings from ipopt and then meaninful output (using a logger). Repair Options ^^^^^^^^^^^^^^ From 9a32b21831be40951aaca03c4c1d9112a1bca4ab Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 09:40:22 -0800 Subject: [PATCH 05/30] black --- pyomo/contrib/mis/mis.py | 9 +++++++-- pyomo/contrib/mis/tests/test_mis.py | 11 ++++++----- pyomo/contrib/mis/tests/trivial_mis.py | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/mis/mis.py b/pyomo/contrib/mis/mis.py index 86832f36933..a4865c0e1a1 100644 --- a/pyomo/contrib/mis/mis.py +++ b/pyomo/contrib/mis/mis.py @@ -8,6 +8,7 @@ See: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf """ + import logging import pyomo.environ as pyo @@ -22,12 +23,13 @@ try: from idaes.core.solvers import get_solver + have_idaes = True except: have_idaes = False logger = logging.getLogger("pyomo.contrib.iis") -logger.setLevel(logging.INFO) +logger.setLevel(logging.INFO) _default_nl_writer = WriterFactory.get_class("nl") @@ -66,7 +68,9 @@ def _apply_to(self, instance, **kwds): v.setub(None) -def compute_infeasibility_explanation(model, solver=None, tee=False, tolerance=1e-8, logger=logger): +def compute_infeasibility_explanation( + model, solver=None, tee=False, tolerance=1e-8, logger=logger +): """ This function attempts to determine why a given model is infeasible. It deploys two main algorithms: @@ -360,6 +364,7 @@ def _get_constraint(modified_model, v): else: raise RuntimeError("Bad var name {v.name}") + """ WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, and National Energy Technology Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. diff --git a/pyomo/contrib/mis/tests/test_mis.py b/pyomo/contrib/mis/tests/test_mis.py index 6776f43f4ce..985bf1911d9 100644 --- a/pyomo/contrib/mis/tests/test_mis.py +++ b/pyomo/contrib/mis/tests/test_mis.py @@ -39,11 +39,12 @@ class TestMIS(unittest.TestCase): ) def test_write_mis_ipopt(self): _test_mis("ipopt") + def test__get_constraint_errors(self): # A not-completely-cyincal way to get the coverage up. m = _get_infeasible_model() # not modified, but who cares? fct = getattr(mis, "_get_constraint") - #fct = mis._get_constraint + # fct = mis._get_constraint m.foo_slack_plus_ = pyo.Var() self.assertRaises(RuntimeError, fct, m, m.foo_slack_plus_) @@ -52,7 +53,7 @@ def test__get_constraint_errors(self): m.foo_bar = pyo.Var() self.assertRaises(RuntimeError, fct, m, m.foo_bar) - + def _check_output(file_name): # pretty simple check for now with open(file_name, "r+") as file1: @@ -71,7 +72,7 @@ def _check_output(file_name): raise RuntimeError(f"Did not find '{nugget}' after '{trigger}' in output") else: pass - + def _test_mis(solver_name): m = _get_infeasible_model() @@ -80,14 +81,14 @@ def _test_mis(solver_name): TempfileManager.push() tmp_path = TempfileManager.create_tempdir() file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") - logger = logging.getLogger(f'test_mis_{solver_name}') + logger = logging.getLogger(f"test_mis_{solver_name}") logger.setLevel(logging.INFO) # create file handler which logs even debug messages print(f"{file_name =}") fh = logging.FileHandler(file_name) fh.setLevel(logging.DEBUG) logger.addHandler(fh) - + mis.compute_infeasibility_explanation(m, opt, logger=logger) _check_output(file_name) diff --git a/pyomo/contrib/mis/tests/trivial_mis.py b/pyomo/contrib/mis/tests/trivial_mis.py index 37f177c7666..29d22d5e5e5 100644 --- a/pyomo/contrib/mis/tests/trivial_mis.py +++ b/pyomo/contrib/mis/tests/trivial_mis.py @@ -1,14 +1,15 @@ import pyomo.environ as pyo + m = pyo.ConcreteModel("Trivial Quad") -m.x = pyo.Var([1,2], bounds=(0,1)) +m.x = pyo.Var([1, 2], bounds=(0, 1)) m.y = pyo.Var(bounds=(0, 1)) m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) from pyomo.contrib.mis.mis import compute_infeasibility_explanation + # if IDAES is installed, compute_infeasibility_explanation doesn't need to be passed a solver # Note: this particular little problem is quadratic # As of 18Feb DLW is not sure the explanation code works with solvers other than ipopt ipopt = pyo.SolverFactory("ipopt") compute_infeasibility_explanation(m, solver=ipopt) - From 20e419648f27448f6400b25cf9b0480f51a5f45c Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Fri, 1 Mar 2024 12:24:05 -0700 Subject: [PATCH 06/30] fixing _get_constraint test --- pyomo/contrib/mis/__init__.py | 2 -- pyomo/contrib/mis/tests/test_mis.py | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/mis/__init__.py b/pyomo/contrib/mis/__init__.py index 5f652020dfa..0235c769467 100644 --- a/pyomo/contrib/mis/__init__.py +++ b/pyomo/contrib/mis/__init__.py @@ -10,5 +10,3 @@ # ___________________________________________________________________________ from pyomo.contrib.mis.mis import compute_infeasibility_explanation -# so the tests can find it -from pyomo.contrib.mis.mis import _get_constraint diff --git a/pyomo/contrib/mis/tests/test_mis.py b/pyomo/contrib/mis/tests/test_mis.py index 985bf1911d9..9948c665f26 100644 --- a/pyomo/contrib/mis/tests/test_mis.py +++ b/pyomo/contrib/mis/tests/test_mis.py @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest import pyomo.environ as pyo import pyomo.contrib.mis as mis +from pyomo.contrib.mis.mis import _get_constraint from pyomo.common.tempfiles import TempfileManager import logging @@ -42,9 +43,8 @@ def test_write_mis_ipopt(self): def test__get_constraint_errors(self): # A not-completely-cyincal way to get the coverage up. - m = _get_infeasible_model() # not modified, but who cares? - fct = getattr(mis, "_get_constraint") - # fct = mis._get_constraint + m = _get_infeasible_model() # not modified + fct = _get_constraint m.foo_slack_plus_ = pyo.Var() self.assertRaises(RuntimeError, fct, m, m.foo_slack_plus_) From f9f7a0d5994c85ebfe3de7c63109c8d2eb483aed Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 13:16:45 -0800 Subject: [PATCH 07/30] removing some spelling errors --- doc/OnlineDocs/contributed_packages/mis.rst | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/mis.rst b/doc/OnlineDocs/contributed_packages/mis.rst index 02117867dd5..c9a1635713c 100644 --- a/doc/OnlineDocs/contributed_packages/mis.rst +++ b/doc/OnlineDocs/contributed_packages/mis.rst @@ -9,18 +9,18 @@ to be used for analysis. It also computes a minimal intractable system as part of the watertap project and is governed by a license shown at the bottom of the ``mis.py``. -The algortithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf +The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf Solver ------ -At the time of this writing, you need to use ipopt even for LPs. +At the time of this writing, you need to use IPopt even for LPs. Quick Start ----------- The file ``trivial_mis.py`` is a tiny example listed at the bottom of -this help file, which references a pyomo model with the Python variable +this help file, which references a Pyomo model with the Python variable `m` and has these lines: .. code-block:: python @@ -33,7 +33,7 @@ this help file, which references a pyomo model with the Python variable This is done instead of solving the problem. .. Note:: - If IDAES is installed, the `solver` keyward argument + If IDAES is installed, the `solver` keyword argument is not needed (the function will use IDAES to find a solver). @@ -42,7 +42,7 @@ Interpreting the Output Assuming the dependencies are installed, file ``trivial_mis.py`` (shown below) will -produce a lot of warnings from ipopt and then meaninful output (using a logger). +produce a lot of warnings from IPopt and then meaningful output (using a logger). Repair Options ^^^^^^^^^^^^^^ @@ -88,15 +88,14 @@ object back from the call to `solve`. In these cases we keep the constraint in t set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, just that “bad things” happen when they’re not included. -Perhpas ideally we would put a constraint in the “guard” set if Ipopt failed to converge, and only put it in the -MIS if Ipopt converged to a point of local infeasibility. However, right now the code generally makes the -assumption that if Ipopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. +Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the +MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the +assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the -constraints in the elastic filter may be feasible -- because Ipopt failed to converge and we assumed that meant the +constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the subproblem was not feasible. -Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when it’s -assumptions are not satisfied. +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when it’s assumptions are not satisfied. trivial_mis.py -------------- From 1ba2437c3b503d55a693a2bba385b53a9c1b75fe Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 13:28:09 -0800 Subject: [PATCH 08/30] more spelling errors removed --- pyomo/contrib/mis/mis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mis/mis.py b/pyomo/contrib/mis/mis.py index a4865c0e1a1..283475fc63b 100644 --- a/pyomo/contrib/mis/mis.py +++ b/pyomo/contrib/mis/mis.py @@ -1,6 +1,6 @@ """ Minimal Intractable System (MIS) finder -Originall written by Ben Knueven as part of the WaterTAP project: +Originally written by Ben Knueven as part of the WaterTAP project: https://github.com/watertap-org/watertap That's why DLW put a huge license notice at the bottom of this file. @@ -150,7 +150,7 @@ def compute_infeasibility_explanation( for v in slack_block.component_data_objects(pyo.Var): v.fix(0) - # start with variable bounds -- these are the easist to interpret + # start with variable bounds -- these are the easiest to interpret for c in modified_model._variable_bounds.component_data_objects( pyo.Constraint, descend_into=True ): @@ -322,7 +322,7 @@ def _get_results_with_value(constr_value_generator, msg=None): elif "ub" in name: msg += f"\tub of var {name[7:]} by {value}\n" else: - raise RuntimeError("unrecongized var name") + raise RuntimeError("unrecognized var name") else: msg += f"\tconstraint: {c_name} by {value}\n" return msg @@ -340,7 +340,7 @@ def _get_results(constr_generator, msg=None): elif "ub" in name: msg += f"\tub of var {name[7:]}\n" else: - raise RuntimeError("unrecongized var name") + raise RuntimeError("unrecognized var name") else: msg += f"\tconstraint: {c_name}\n" return msg From 9dc6fc892ff225bbcf65227098e19532e2ba146c Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 13:54:21 -0800 Subject: [PATCH 09/30] update typos.toml for mis --- .github/workflows/typos.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 23f94fc8afd..be31d21a641 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,4 +40,7 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" +# contrib package named mis and the acronym whence the name comes +mis = "mis" +MIS = "MIS" # AS NEEDED: Add More Words Below From b3cbc0ef561eb17711a7b5061aa17f0eb38c88ea Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 14:07:15 -0800 Subject: [PATCH 10/30] I forgot to push the __init__.py file in tests --- pyomo/contrib/mis/tests/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyomo/contrib/mis/tests/__init__.py diff --git a/pyomo/contrib/mis/tests/__init__.py b/pyomo/contrib/mis/tests/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/pyomo/contrib/mis/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ From c6376aa9bb28fca7afd47c919da49a9861f77948 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 1 Mar 2024 14:33:54 -0800 Subject: [PATCH 11/30] a little documentation cleanup --- doc/OnlineDocs/contributed_packages/mis.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/mis.rst b/doc/OnlineDocs/contributed_packages/mis.rst index c9a1635713c..c187a13ae5f 100644 --- a/doc/OnlineDocs/contributed_packages/mis.rst +++ b/doc/OnlineDocs/contributed_packages/mis.rst @@ -40,7 +40,7 @@ this help file, which references a Pyomo model with the Python variable Interpreting the Output ----------------------- -Assuming the dependencies are installed, file ``trivial_mis.py`` +Assuming the dependencies are installed, running ``trivial_mis.py`` (shown below) will produce a lot of warnings from IPopt and then meaningful output (using a logger). @@ -50,7 +50,7 @@ Repair Options This output for the trivial example shows three independent ways that the model could be rendered feasible: -.. raw:: +.. code-block:: text Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: ub of var x[1] by 4.464126126706818e-05 @@ -69,7 +69,7 @@ Minimal Intractable System (MIS) This output shows a minimal intractable system: -.. raw:: +.. code-block:: text Computed Minimal Intractable System (MIS)! Constraints / bounds in MIS: From 90bfcf3a0c39736ff5e029d5834f37f8c0d37f35 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Sun, 17 Mar 2024 16:15:10 -0700 Subject: [PATCH 12/30] moved mis to be part of iis --- doc/OnlineDocs/conf.py | 1 + doc/OnlineDocs/contributed_packages/iis.rst | 129 ++++++++++++++++++ doc/OnlineDocs/contributed_packages/index.rst | 1 - doc/OnlineDocs/contributed_packages/mis.rst | 114 ---------------- pyomo/contrib/iis/__init__.py | 1 + pyomo/contrib/{mis => iis}/mis.py | 0 pyomo/contrib/{mis => iis}/tests/test_mis.py | 2 +- .../contrib/{mis => iis}/tests/trivial_mis.py | 0 pyomo/contrib/mis/__init__.py | 12 -- pyomo/contrib/mis/tests/__init__.py | 10 -- 10 files changed, 132 insertions(+), 138 deletions(-) delete mode 100644 doc/OnlineDocs/contributed_packages/mis.rst rename pyomo/contrib/{mis => iis}/mis.py (100%) rename pyomo/contrib/{mis => iis}/tests/test_mis.py (98%) rename pyomo/contrib/{mis => iis}/tests/trivial_mis.py (100%) delete mode 100644 pyomo/contrib/mis/__init__.py delete mode 100644 pyomo/contrib/mis/tests/__init__.py diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 1aab4cd76c2..a06ccfbc9bd 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -84,6 +84,7 @@ 'sphinx.ext.todo', 'sphinx_copybutton', 'enum_tools.autoenum', + 'sphinx.ext.autosectionlabel', #'sphinx.ext.githubpages', ] diff --git a/doc/OnlineDocs/contributed_packages/iis.rst b/doc/OnlineDocs/contributed_packages/iis.rst index 98cb9e30771..ffe48424df7 100644 --- a/doc/OnlineDocs/contributed_packages/iis.rst +++ b/doc/OnlineDocs/contributed_packages/iis.rst @@ -1,6 +1,135 @@ +Infeasibility Diagnostics +!!!!!!!!!!!!!!!!!!!!!!!!! + +There are two closely related tools for infeasibility diagnosis: + + - :ref:`Infeasible Irreducible System (IIS) Tool` + - :ref:`Minimal Intractable System finder (MIS) Tool` + +The first simply provides a conduit for solvers that compute an +infeasible irreducible system (e.g., Cplex, Gurobi, or Xpress). The +second provides similar functionality, but uses the ``mis`` package +contributed to Pyomo. + + Infeasible Irreducible System (IIS) Tool ======================================== .. automodule:: pyomo.contrib.iis.iis .. autofunction:: pyomo.contrib.iis.write_iis + +Minimal Intractable System finder (MIS) Tool +============================================ + +The file ``mis.py`` finds sets of actions that each, independently, +would result in feasibility. The zero-tolerance is whatever the +solver uses, so users may want to post-process output if it is going +to be used for analysis. It also computes a minimal intractable system +(which is not guaranteed to be unique). It was written by Ben Knueven +as part of the watertap project and is governed by a license shown +at the bottom of the ``mis.py``. + +The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf + +Solver +------ + +At the time of this writing, you need to use IPopt even for LPs. + +Quick Start +----------- + +The file ``trivial_mis.py`` is a tiny example listed at the bottom of +this help file, which references a Pyomo model with the Python variable +`m` and has these lines: + +.. code-block:: python + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) + +.. Note:: + This is done instead of solving the problem. + +.. Note:: + If IDAES is installed, the `solver` keyword argument + is not needed (the function will use IDAES to find + a solver). + +Interpreting the Output +----------------------- + +Assuming the dependencies are installed, running ``trivial_mis.py`` +(shown below) will +produce a lot of warnings from IPopt and then meaningful output (using a logger). + +Repair Options +^^^^^^^^^^^^^^ + +This output for the trivial example shows three independent ways that the model could be rendered feasible: + + +.. code-block:: text + + Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: + ub of var x[1] by 4.464126126706818e-05 + lb of var x[2] by 0.9999553410114216 + Another feasible solution was found with only the following variable bounds relaxed: + lb of var x[1] by 0.7071067726864677 + ub of var x[2] by 0.41421355687130673 + ub of var y by 0.7071067651855212 + Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: + constraint: c by 0.9999999861866736 + + +Minimal Intractable System (MIS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This output shows a minimal intractable system: + + +.. code-block:: text + + Computed Minimal Intractable System (MIS)! + Constraints / bounds in MIS: + lb of var x[2] + lb of var x[1] + constraint: c + +Constraints / bounds in guards for stability +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This part of the report is for nonlinear programs (NLPs). + +When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver +to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` +object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the +set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, +just that “bad things” happen when they’re not included. + +Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the +MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the +assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. +Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the +constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the +subproblem was not feasible. + +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when it’s assumptions are not satisfied. + +trivial_mis.py +-------------- + +.. code-block:: python + + import pyomo.environ as pyo + m = pyo.ConcreteModel("Trivial Quad") + m.x = pyo.Var([1,2], bounds=(0,1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) diff --git a/doc/OnlineDocs/contributed_packages/index.rst b/doc/OnlineDocs/contributed_packages/index.rst index 33e1c7f851d..b1d9cbbad3b 100644 --- a/doc/OnlineDocs/contributed_packages/index.rst +++ b/doc/OnlineDocs/contributed_packages/index.rst @@ -30,7 +30,6 @@ Contributed packages distributed with Pyomo: pyros.rst sensitivity_toolbox.rst trustregion.rst - mis.rst Contributed Pyomo interfaces to other packages: diff --git a/doc/OnlineDocs/contributed_packages/mis.rst b/doc/OnlineDocs/contributed_packages/mis.rst deleted file mode 100644 index c187a13ae5f..00000000000 --- a/doc/OnlineDocs/contributed_packages/mis.rst +++ /dev/null @@ -1,114 +0,0 @@ -Minimal Intractable System finder -================================= - -The file ``mis.py`` finds sets of actions that each, independently, -would result in feasibility. The zero-tolerance is whatever the -solver uses, so users may want to post-process output if it is going -to be used for analysis. It also computes a minimal intractable system -(which is not guaranteed to be unique). It was written by Ben Knueven -as part of the watertap project and is governed by a license shown -at the bottom of the ``mis.py``. - -The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf - -Solver ------- - -At the time of this writing, you need to use IPopt even for LPs. - -Quick Start ------------ - -The file ``trivial_mis.py`` is a tiny example listed at the bottom of -this help file, which references a Pyomo model with the Python variable -`m` and has these lines: - -.. code-block:: python - - from pyomo.contrib.mis import compute_infeasibility_explanation - ipopt = pyo.SolverFactory("ipopt") - compute_infeasibility_explanation(m, solver=ipopt) - -.. Note:: - This is done instead of solving the problem. - -.. Note:: - If IDAES is installed, the `solver` keyword argument - is not needed (the function will use IDAES to find - a solver). - -Interpreting the Output ------------------------ - -Assuming the dependencies are installed, running ``trivial_mis.py`` -(shown below) will -produce a lot of warnings from IPopt and then meaningful output (using a logger). - -Repair Options -^^^^^^^^^^^^^^ - -This output for the trivial example shows three independent ways that the model could be rendered feasible: - - -.. code-block:: text - - Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: - ub of var x[1] by 4.464126126706818e-05 - lb of var x[2] by 0.9999553410114216 - Another feasible solution was found with only the following variable bounds relaxed: - lb of var x[1] by 0.7071067726864677 - ub of var x[2] by 0.41421355687130673 - ub of var y by 0.7071067651855212 - Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: - constraint: c by 0.9999999861866736 - - -Minimal Intractable System (MIS) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This output shows a minimal intractable system: - - -.. code-block:: text - - Computed Minimal Intractable System (MIS)! - Constraints / bounds in MIS: - lb of var x[2] - lb of var x[1] - constraint: c - -Constraints / bounds in guards for stability -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This part of the report is for nonlinear programs (NLPs). - -When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver -to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` -object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the -set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, -just that “bad things” happen when they’re not included. - -Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the -MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the -assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. -Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the -constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the -subproblem was not feasible. - -Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when it’s assumptions are not satisfied. - -trivial_mis.py --------------- - -.. code-block:: python - - import pyomo.environ as pyo - m = pyo.ConcreteModel("Trivial Quad") - m.x = pyo.Var([1,2], bounds=(0,1)) - m.y = pyo.Var(bounds=(0, 1)) - m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) - m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) - - from pyomo.contrib.mis import compute_infeasibility_explanation - ipopt = pyo.SolverFactory("ipopt") - compute_infeasibility_explanation(m, solver=ipopt) diff --git a/pyomo/contrib/iis/__init__.py b/pyomo/contrib/iis/__init__.py index e8d6a7ac2c3..961ac576d42 100644 --- a/pyomo/contrib/iis/__init__.py +++ b/pyomo/contrib/iis/__init__.py @@ -10,3 +10,4 @@ # ___________________________________________________________________________ from pyomo.contrib.iis.iis import write_iis +from pyomo.contrib.iis.mis import compute_infeasibility_explanation diff --git a/pyomo/contrib/mis/mis.py b/pyomo/contrib/iis/mis.py similarity index 100% rename from pyomo/contrib/mis/mis.py rename to pyomo/contrib/iis/mis.py diff --git a/pyomo/contrib/mis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py similarity index 98% rename from pyomo/contrib/mis/tests/test_mis.py rename to pyomo/contrib/iis/tests/test_mis.py index 9948c665f26..317b41b82f3 100644 --- a/pyomo/contrib/mis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -12,7 +12,7 @@ import pyomo.common.unittest as unittest import pyomo.environ as pyo import pyomo.contrib.mis as mis -from pyomo.contrib.mis.mis import _get_constraint +from pyomo.contrib.iis.mis import _get_constraint from pyomo.common.tempfiles import TempfileManager import logging diff --git a/pyomo/contrib/mis/tests/trivial_mis.py b/pyomo/contrib/iis/tests/trivial_mis.py similarity index 100% rename from pyomo/contrib/mis/tests/trivial_mis.py rename to pyomo/contrib/iis/tests/trivial_mis.py diff --git a/pyomo/contrib/mis/__init__.py b/pyomo/contrib/mis/__init__.py deleted file mode 100644 index 0235c769467..00000000000 --- a/pyomo/contrib/mis/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.contrib.mis.mis import compute_infeasibility_explanation diff --git a/pyomo/contrib/mis/tests/__init__.py b/pyomo/contrib/mis/tests/__init__.py deleted file mode 100644 index a4a626013c4..00000000000 --- a/pyomo/contrib/mis/tests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ From 5211a9bc35b3f9094139a9f07f4f65b338b1d362 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Sun, 17 Mar 2024 16:25:41 -0700 Subject: [PATCH 13/30] correct bad import in mis test --- pyomo/contrib/iis/tests/test_mis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 317b41b82f3..f93f60feeec 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -11,7 +11,7 @@ import pyomo.common.unittest as unittest import pyomo.environ as pyo -import pyomo.contrib.mis as mis +import pyomo.contrib.iis.mis as mis from pyomo.contrib.iis.mis import _get_constraint from pyomo.common.tempfiles import TempfileManager From 2674690e01946fabf23bee8c2c665f76b2d94835 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Sun, 17 Mar 2024 17:39:01 -0700 Subject: [PATCH 14/30] I didn't realize it would run every py file in the test directory --- pyomo/contrib/iis/tests/trivial_mis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/iis/tests/trivial_mis.py b/pyomo/contrib/iis/tests/trivial_mis.py index 29d22d5e5e5..3f65e7d21c3 100644 --- a/pyomo/contrib/iis/tests/trivial_mis.py +++ b/pyomo/contrib/iis/tests/trivial_mis.py @@ -6,7 +6,7 @@ m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) -from pyomo.contrib.mis.mis import compute_infeasibility_explanation +from pyomo.contrib.iis.mis import compute_infeasibility_explanation # if IDAES is installed, compute_infeasibility_explanation doesn't need to be passed a solver # Note: this particular little problem is quadratic From 09ad1adc173d65dc8957bb6841f64fea30833776 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Mon, 18 Mar 2024 10:50:33 -0700 Subject: [PATCH 15/30] trying to get the Windows tests to pass by explicitly releasing the logger file handle --- pyomo/contrib/iis/tests/test_mis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index f93f60feeec..39ad9615e3a 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -91,7 +91,9 @@ def _test_mis(solver_name): mis.compute_infeasibility_explanation(m, opt, logger=logger) _check_output(file_name) - + # logging.getLogger().removeHandler(logging.getLogger().handlers[0]) + logger.removeHandler(logger.handlers[0]) + TempfileManager.pop() From 485cddc1805e9af0815b968c8ae226e3a524580c Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Mon, 18 Mar 2024 12:39:32 -0700 Subject: [PATCH 16/30] run black on test_mis.py --- pyomo/contrib/iis/tests/test_mis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 39ad9615e3a..adf44a189af 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -93,7 +93,7 @@ def _test_mis(solver_name): _check_output(file_name) # logging.getLogger().removeHandler(logging.getLogger().handlers[0]) logger.removeHandler(logger.handlers[0]) - + TempfileManager.pop() From 9f624d66d723043aa5297212dc20d67089c65001 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Fri, 29 Mar 2024 15:11:26 -0700 Subject: [PATCH 17/30] trying to manage the temp dir using the tempfilemanager as a context --- pyomo/contrib/iis/tests/test_mis.py | 36 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index adf44a189af..5a1b5f06d9d 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -78,23 +78,25 @@ def _test_mis(solver_name): m = _get_infeasible_model() opt = pyo.SolverFactory(solver_name) - TempfileManager.push() - tmp_path = TempfileManager.create_tempdir() - file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") - logger = logging.getLogger(f"test_mis_{solver_name}") - logger.setLevel(logging.INFO) - # create file handler which logs even debug messages - print(f"{file_name =}") - fh = logging.FileHandler(file_name) - fh.setLevel(logging.DEBUG) - logger.addHandler(fh) - - mis.compute_infeasibility_explanation(m, opt, logger=logger) - _check_output(file_name) - # logging.getLogger().removeHandler(logging.getLogger().handlers[0]) - logger.removeHandler(logger.handlers[0]) - - TempfileManager.pop() + ####TempfileManager.push() + ####tmp_path = TempfileManager.create_tempdir() + with TempfileManager.new_context() as tmpmgr: + tmp_path = tmpmgr.mkdtemp() + file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") + logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) + # create file handler which logs even debug messages + print(f"{file_name =}") + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + # logging.getLogger().removeHandler(logging.getLogger().handlers[0]) + logger.removeHandler(logger.handlers[0]) + + ####TempfileManager.pop() if __name__ == "__main__": From 2b7bfe7f854bb7e9ccf871dfa1ed9f96d4b0e62f Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Fri, 29 Mar 2024 18:50:55 -0700 Subject: [PATCH 18/30] catch the error that kills windows tests --- pyomo/contrib/iis/tests/test_mis.py | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 5a1b5f06d9d..452d1b4e773 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -20,7 +20,7 @@ def _get_infeasible_model(): - m = pyo.ConcreteModel() + m = pyo.ConcreteModel("trivial4test") m.x = pyo.Var(within=pyo.Binary) m.y = pyo.Var(within=pyo.NonNegativeReals) @@ -78,25 +78,25 @@ def _test_mis(solver_name): m = _get_infeasible_model() opt = pyo.SolverFactory(solver_name) - ####TempfileManager.push() - ####tmp_path = TempfileManager.create_tempdir() - with TempfileManager.new_context() as tmpmgr: - tmp_path = tmpmgr.mkdtemp() - file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") - logger = logging.getLogger(f"test_mis_{solver_name}") - logger.setLevel(logging.INFO) - # create file handler which logs even debug messages - print(f"{file_name =}") - fh = logging.FileHandler(file_name) - fh.setLevel(logging.DEBUG) - logger.addHandler(fh) - - mis.compute_infeasibility_explanation(m, opt, logger=logger) - _check_output(file_name) - # logging.getLogger().removeHandler(logging.getLogger().handlers[0]) - logger.removeHandler(logger.handlers[0]) - - ####TempfileManager.pop() + # This test seems to fail on Windows as it unlinks the tempfile, so live with it + try: + with TempfileManager.new_context() as tmpmgr: + print("start") + tmp_path = tmpmgr.mkdtemp() + file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") + logger = logging.getLogger(f"test_mis_{solver_name}") + ###logger.setLevel(logging.INFO) + # create file handler which logs even debug messages + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + + except PermissionError: + print("PerimssionError allowed for log file during test (Windows)") + if __name__ == "__main__": From 7c040228ead83e9a9cec7e3703bcbf69b8f51f74 Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Fri, 29 Mar 2024 18:54:57 -0700 Subject: [PATCH 19/30] run black again --- pyomo/contrib/iis/tests/test_mis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 452d1b4e773..0561d647310 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -98,6 +98,5 @@ def _test_mis(solver_name): print("PerimssionError allowed for log file during test (Windows)") - if __name__ == "__main__": unittest.main() From 1629bd75fad70a10ece2bf9d1a6eff9ef5ed8967 Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Fri, 29 Mar 2024 19:37:39 -0700 Subject: [PATCH 20/30] windows started passing, but linux failing; one quick check to see if logging.info helps: --- pyomo/contrib/iis/tests/test_mis.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 0561d647310..e4535e021b3 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -79,14 +79,24 @@ def _test_mis(solver_name): opt = pyo.SolverFactory(solver_name) # This test seems to fail on Windows as it unlinks the tempfile, so live with it - try: + # On a Windows machine, we will not use a temp dir and just try to delete the log file + if os.name == "nt": + print("we have nt") + file_name = f"{solver_name}_mis.log" + logger = logging.getLogger(f"test_mis_{solver_name}") + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + + else: # not windows with TempfileManager.new_context() as tmpmgr: - print("start") tmp_path = tmpmgr.mkdtemp() file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") logger = logging.getLogger(f"test_mis_{solver_name}") - ###logger.setLevel(logging.INFO) - # create file handler which logs even debug messages + logger.setlevel(logging.INFO) fh = logging.FileHandler(file_name) fh.setLevel(logging.DEBUG) logger.addHandler(fh) @@ -94,9 +104,6 @@ def _test_mis(solver_name): mis.compute_infeasibility_explanation(m, opt, logger=logger) _check_output(file_name) - except PermissionError: - print("PerimssionError allowed for log file during test (Windows)") - if __name__ == "__main__": unittest.main() From daeed6efaf6c9fe9b7c39c7d1140ffda70c9a98a Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Fri, 29 Mar 2024 19:43:51 -0700 Subject: [PATCH 21/30] run black again --- pyomo/contrib/iis/tests/test_mis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index e4535e021b3..4ec232a80a3 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -90,7 +90,7 @@ def _test_mis(solver_name): mis.compute_infeasibility_explanation(m, opt, logger=logger) _check_output(file_name) - + else: # not windows with TempfileManager.new_context() as tmpmgr: tmp_path = tmpmgr.mkdtemp() From ce6bac4960654bec32a15d7c7ee9c926588bc64a Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Sat, 30 Mar 2024 09:01:27 -0700 Subject: [PATCH 22/30] On windows we are just going to have to leave a log file from the test --- pyomo/contrib/iis/tests/test_mis.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 4ec232a80a3..0b2185f3d29 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -82,21 +82,23 @@ def _test_mis(solver_name): # On a Windows machine, we will not use a temp dir and just try to delete the log file if os.name == "nt": print("we have nt") - file_name = f"{solver_name}_mis.log" + file_name = f"_test_mis_{solver_name}.log" logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) fh = logging.FileHandler(file_name) fh.setLevel(logging.DEBUG) logger.addHandler(fh) mis.compute_infeasibility_explanation(m, opt, logger=logger) _check_output(file_name) + # os.remove(file_name) cannot remove it on Windows. Still in use. else: # not windows with TempfileManager.new_context() as tmpmgr: tmp_path = tmpmgr.mkdtemp() - file_name = os.path.join(tmp_path, f"{solver_name}_mis.log") + file_name = os.path.join(tmp_path, f"_test_mis_{solver_name}.log") logger = logging.getLogger(f"test_mis_{solver_name}") - logger.setlevel(logging.INFO) + logger.setLevel(logging.INFO) fh = logging.FileHandler(file_name) fh.setLevel(logging.DEBUG) logger.addHandler(fh) From f35bce55abeee05834b7420c6612317f72951b52 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Tue, 16 Apr 2024 17:56:58 -0700 Subject: [PATCH 23/30] add a test for a feasible model --- pyomo/contrib/iis/tests/test_mis.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 0b2185f3d29..37f27ad5192 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -32,6 +32,15 @@ def _get_infeasible_model(): return m +def _get_feasible_model(): + m = pyo.ConcreteModel("Trivial Feasible Quad") + m.x = pyo.Var([1, 2], bounds=(0, 1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] >= -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + return m + class TestMIS(unittest.TestCase): @unittest.skipUnless( @@ -53,6 +62,12 @@ def test__get_constraint_errors(self): m.foo_bar = pyo.Var() self.assertRaises(RuntimeError, fct, m, m.foo_bar) + def test_feasible_model(self): + m = _get_feasible_model() + opt = pyo.SolverFactory("ipopt") + self.assertRaises(Exception, + mis.compute_infeasibility_explanation, m, opt) + def _check_output(file_name): # pretty simple check for now From 57f52a078f59de00a01869ee6fca415aa07a14a5 Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Tue, 16 Apr 2024 18:01:56 -0700 Subject: [PATCH 24/30] Update pyomo/contrib/iis/mis.py Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/contrib/iis/mis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py index 283475fc63b..1d78aeb434b 100644 --- a/pyomo/contrib/iis/mis.py +++ b/pyomo/contrib/iis/mis.py @@ -165,7 +165,7 @@ def compute_infeasibility_explanation( # TODO: Elasticizing too much at once seems to cause Ipopt trouble. # After an initial sweep, we should just fix one elastic variable # and put everything else on a stack of "constraints to elasticize". - # We elastisize one constraint at a time and fix one constraint at a time. + # We elasticize one constraint at a time and fix one constraint at a time. # After fixing an elastic variable, we elasticize a single constraint it # appears in and put the remaining constraints on the stack. If the resulting problem # is feasible, we keep going "down the tree". If the resulting problem is From ed526db0bd631ee5a5e853da007bb5f45dcaf31f Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Tue, 16 Apr 2024 18:25:02 -0700 Subject: [PATCH 25/30] Changes suggested by Miranda --- doc/OnlineDocs/contributed_packages/iis.rst | 7 ++- pyomo/contrib/iis/mis.py | 59 ++++++++++----------- pyomo/contrib/iis/tests/trivial_mis.py | 13 ++++- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/iis.rst b/doc/OnlineDocs/contributed_packages/iis.rst index ffe48424df7..c6e588720b4 100644 --- a/doc/OnlineDocs/contributed_packages/iis.rst +++ b/doc/OnlineDocs/contributed_packages/iis.rst @@ -28,7 +28,7 @@ solver uses, so users may want to post-process output if it is going to be used for analysis. It also computes a minimal intractable system (which is not guaranteed to be unique). It was written by Ben Knueven as part of the watertap project and is governed by a license shown -at the bottom of the ``mis.py``. +at the top of ``mis.py``. The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf @@ -54,9 +54,8 @@ this help file, which references a Pyomo model with the Python variable This is done instead of solving the problem. .. Note:: - If IDAES is installed, the `solver` keyword argument - is not needed (the function will use IDAES to find - a solver). + IDAES users can pass ``get_solver()`` imported from ``ideas.core.solvers`` + as the solver. Interpreting the Output ----------------------- diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py index 1d78aeb434b..6b7eab63ef2 100644 --- a/pyomo/contrib/iis/mis.py +++ b/pyomo/contrib/iis/mis.py @@ -1,8 +1,33 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, and National Energy Technology Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + Neither the name of the University of California, Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, National Energy Technology Laboratory, U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, without imposing a separate written license agreement for such Enhancements, then you hereby grant the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare derivative works, incorporate into other computer software, distribute, and sublicense such enhancements or derivative works thereof, in binary and source code form. +""" """ Minimal Intractable System (MIS) finder Originally written by Ben Knueven as part of the WaterTAP project: https://github.com/watertap-org/watertap -That's why DLW put a huge license notice at the bottom of this file. +That's why this file has the watertap copyright notice. copied by DLW 18Feb2024 and edited @@ -21,13 +46,6 @@ from pyomo.opt import WriterFactory -try: - from idaes.core.solvers import get_solver - - have_idaes = True -except: - have_idaes = False - logger = logging.getLogger("pyomo.contrib.iis") logger.setLevel(logging.INFO) @@ -100,10 +118,7 @@ def compute_infeasibility_explanation( modified_model = model.clone() if solver is None: - if have_idaes: - solver = get_solver() - else: - raise ValueError("solver needed unless IDAES is installed") + raise ValueError("A solver must be supplied") elif isinstance(solver, str): solver = pyo.SolverFactory(solver) else: @@ -283,12 +298,11 @@ def _constraint_generator(): constr.deactivate() for var, val in _modified_model_value_cache.items(): var.set_value(val, skip_validation=True) + math_failure = False try: results = solver.solve(modified_model, tee=tee) except: math_failure = True - else: - math_failure = False if math_failure: constr.activate() @@ -363,20 +377,3 @@ def _get_constraint(modified_model, v): return constr else: raise RuntimeError("Bad var name {v.name}") - - -""" -WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, and National Energy Technology Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - - Neither the name of the University of California, Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, National Energy Technology Laboratory, U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, without imposing a separate written license agreement for such Enhancements, then you hereby grant the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare derivative works, incorporate into other computer software, distribute, and sublicense such enhancements or derivative works thereof, in binary and source code form. -""" diff --git a/pyomo/contrib/iis/tests/trivial_mis.py b/pyomo/contrib/iis/tests/trivial_mis.py index 3f65e7d21c3..4cf0dd7a357 100644 --- a/pyomo/contrib/iis/tests/trivial_mis.py +++ b/pyomo/contrib/iis/tests/trivial_mis.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ import pyomo.environ as pyo m = pyo.ConcreteModel("Trivial Quad") @@ -8,8 +18,7 @@ from pyomo.contrib.iis.mis import compute_infeasibility_explanation -# if IDAES is installed, compute_infeasibility_explanation doesn't need to be passed a solver # Note: this particular little problem is quadratic -# As of 18Feb DLW is not sure the explanation code works with solvers other than ipopt +# As of 18Feb2024 DLW is not sure the explanation code works with solvers other than ipopt ipopt = pyo.SolverFactory("ipopt") compute_infeasibility_explanation(m, solver=ipopt) From 1739d9919d7350f85411f0317bb4527375a3706a Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Tue, 16 Apr 2024 18:28:54 -0700 Subject: [PATCH 26/30] run black again --- pyomo/contrib/iis/tests/test_mis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 37f27ad5192..266673f2ec9 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -32,6 +32,7 @@ def _get_infeasible_model(): return m + def _get_feasible_model(): m = pyo.ConcreteModel("Trivial Feasible Quad") m.x = pyo.Var([1, 2], bounds=(0, 1)) @@ -65,9 +66,8 @@ def test__get_constraint_errors(self): def test_feasible_model(self): m = _get_feasible_model() opt = pyo.SolverFactory("ipopt") - self.assertRaises(Exception, - mis.compute_infeasibility_explanation, m, opt) - + self.assertRaises(Exception, mis.compute_infeasibility_explanation, m, opt) + def _check_output(file_name): # pretty simple check for now From a4a94962f24d86d6297d00953dd2ad62e5032ccb Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 11 Apr 2024 13:58:30 -0600 Subject: [PATCH 27/30] simplifying the code --- pyomo/contrib/iis/mis.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py index 6b7eab63ef2..bc0bccdd5cf 100644 --- a/pyomo/contrib/iis/mis.py +++ b/pyomo/contrib/iis/mis.py @@ -49,8 +49,6 @@ logger = logging.getLogger("pyomo.contrib.iis") logger.setLevel(logging.INFO) -_default_nl_writer = WriterFactory.get_class("nl") - class _VariableBoundsAsConstraints(IsomorphicTransformation): """Replace all variables bounds and domain information with constraints. @@ -87,7 +85,7 @@ def _apply_to(self, instance, **kwds): def compute_infeasibility_explanation( - model, solver=None, tee=False, tolerance=1e-8, logger=logger + model, solver, tee=False, tolerance=1e-8, logger=logger ): """ This function attempts to determine why a given model is infeasible. It deploys @@ -105,7 +103,7 @@ def compute_infeasibility_explanation( Args ---- model: A pyomo block - solver (optional): A pyomo solver, a string, or None + solver: A pyomo solver object or a string for SolverFactory tee (optional): Display intermediate solves conducted (False) tolerance (optional): The feasibility tolerance to use when declaring a constraint feasible (1e-08) @@ -267,10 +265,6 @@ def _constraint_generator(): ) # Phase 2 -- deletion filter - # TODO: the model created here seems to mess with the nl_v2 - # writer sometimes. So we temporarily switch to nl_v1 writer. - WriterFactory.register("nl")(WriterFactory.get_class("nl_v1")) - # remove slacks by fixing them to 0 for v in slack_block.component_data_objects(pyo.Var): v.fix(0) @@ -319,8 +313,6 @@ def _constraint_generator(): msg += "Constraints / bounds in guards for stability:" msg = _get_results(guards, msg) - WriterFactory.register("nl")(_default_nl_writer) - logger.info(msg) From ac3d587ee2d3d964fd5c54f993ce5749e2392683 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Sun, 21 Apr 2024 15:36:34 -0700 Subject: [PATCH 28/30] take care of Miranda's helpful comments --- doc/OnlineDocs/contributed_packages/iis.rst | 5 +++-- pyomo/contrib/iis/mis.py | 12 +++++++----- pyomo/contrib/iis/tests/test_mis.py | 9 ++++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/iis.rst b/doc/OnlineDocs/contributed_packages/iis.rst index c6e588720b4..fa97c2f8c61 100644 --- a/doc/OnlineDocs/contributed_packages/iis.rst +++ b/doc/OnlineDocs/contributed_packages/iis.rst @@ -27,7 +27,8 @@ would result in feasibility. The zero-tolerance is whatever the solver uses, so users may want to post-process output if it is going to be used for analysis. It also computes a minimal intractable system (which is not guaranteed to be unique). It was written by Ben Knueven -as part of the watertap project and is governed by a license shown +as part of the watertap project (https://github.com/watertap-org/watertap) +and is therefore governed by a license shown at the top of ``mis.py``. The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf @@ -115,7 +116,7 @@ Hence for difficult NLPs even the “Phase 1” may “fail” – in that when constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the subproblem was not feasible. -Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when it’s assumptions are not satisfied. +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when its assumptions are not satisfied. trivial_mis.py -------------- diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py index bc0bccdd5cf..7044d483f65 100644 --- a/pyomo/contrib/iis/mis.py +++ b/pyomo/contrib/iis/mis.py @@ -58,9 +58,9 @@ class _VariableBoundsAsConstraints(IsomorphicTransformation): def _apply_to(self, instance, **kwds): - boundconstrblockname = unique_component_name(instance, "_variable_bounds") - instance.add_component(boundconstrblockname, pyo.Block()) - boundconstrblock = instance.component(boundconstrblockname) + bound_constr_block_name = unique_component_name(instance, "_variable_bounds") + instance.add_component(bound_constr_block_name, pyo.Block()) + bound_constr_block = instance.component(bound_constr_block_name) for v in instance.component_data_objects(pyo.Var, descend_into=True): if v.fixed: @@ -72,11 +72,11 @@ def _apply_to(self, instance, **kwds): if lb is not None: con_name = "lb_for_" + var_name con = pyo.Constraint(expr=(lb, v, None)) - boundconstrblock.add_component(con_name, con) + bound_constr_block.add_component(con_name, con) if ub is not None: con_name = "ub_for_" + var_name con = pyo.Constraint(expr=(None, v, ub)) - boundconstrblock.add_component(con_name, con) + bound_constr_block.add_component(con_name, con) # now we deactivate the variable bounds / domain v.domain = pyo.Reals @@ -317,6 +317,7 @@ def _constraint_generator(): def _get_results_with_value(constr_value_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long if msg is None: msg = "" for c, value in constr_value_generator: @@ -335,6 +336,7 @@ def _get_results_with_value(constr_value_generator, msg=None): def _get_results(constr_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long if msg is None: msg = "" for c in constr_generator: diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py index 266673f2ec9..bbdb2367016 100644 --- a/pyomo/contrib/iis/tests/test_mis.py +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -52,7 +52,7 @@ def test_write_mis_ipopt(self): _test_mis("ipopt") def test__get_constraint_errors(self): - # A not-completely-cyincal way to get the coverage up. + # A not-completely-cynical way to get the coverage up. m = _get_infeasible_model() # not modified fct = _get_constraint @@ -76,14 +76,14 @@ def _check_output(file_name): trigger = "Constraints / bounds in MIS:" nugget = "lb of var y" live = False # (long i) - wewin = False + found_nugget = False for line in lines: if trigger in line: live = True if live: if nugget in line: - wewin = True - if not wewin: + found_nugget = True + if not found_nugget: raise RuntimeError(f"Did not find '{nugget}' after '{trigger}' in output") else: pass @@ -96,7 +96,6 @@ def _test_mis(solver_name): # This test seems to fail on Windows as it unlinks the tempfile, so live with it # On a Windows machine, we will not use a temp dir and just try to delete the log file if os.name == "nt": - print("we have nt") file_name = f"_test_mis_{solver_name}.log" logger = logging.getLogger(f"test_mis_{solver_name}") logger.setLevel(logging.INFO) From f4f4b2ced7245b8ab816ed4d2e769a144d3347f7 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Wed, 1 May 2024 08:18:14 -0700 Subject: [PATCH 29/30] add sorely needed f to format error messages --- pyomo/contrib/iis/mis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py index 7044d483f65..a309cff6477 100644 --- a/pyomo/contrib/iis/mis.py +++ b/pyomo/contrib/iis/mis.py @@ -359,15 +359,15 @@ def _get_constraint(modified_model, v): constr = modified_model.find_component(v.local_name[len("_slack_plus_") :]) if constr is None: raise RuntimeError( - "Bad constraint name {v.local_name[len('_slack_plus_'):]}" + f"Bad constraint name {v.local_name[len('_slack_plus_'):]}" ) return constr elif "_slack_minus_" in v.name: constr = modified_model.find_component(v.local_name[len("_slack_minus_") :]) if constr is None: raise RuntimeError( - "Bad constraint name {v.local_name[len('_slack_minus_'):]}" + f"Bad constraint name {v.local_name[len('_slack_minus_'):]}" ) return constr else: - raise RuntimeError("Bad var name {v.name}") + raise RuntimeError(f"Bad var name {v.name}") From 21aa8c84f2f8f66196c017a5249c23d01acac9e9 Mon Sep 17 00:00:00 2001 From: "David L. Woodruff" Date: Wed, 1 May 2024 08:26:20 -0700 Subject: [PATCH 30/30] added suggestions from R. Parker to the comments --- pyomo/contrib/iis/mis.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py index a309cff6477..6b6cca8e29c 100644 --- a/pyomo/contrib/iis/mis.py +++ b/pyomo/contrib/iis/mis.py @@ -111,6 +111,7 @@ def compute_infeasibility_explanation( A logger for messages. Uses pyomo.contrib.mis logger by default. """ + # Suggested enhancement: It might be useful to return sets of names for each set of relaxed components, as well as the final minimal infeasible system # hold the original harmless modified_model = model.clone() @@ -155,6 +156,9 @@ def compute_infeasibility_explanation( # modeler to sift through. We could try to sort the constraints # such that we look for those with linear coefficients `1` on # some term and leave those be. + # Alternatively, we could apply this tool to a version of the + # model that has as many as possible of these constraints + # "substituted out". # move the variable bounds to the constraints _VariableBoundsAsConstraints().apply_to(modified_model)