Skip to content

MindtPy fails to return solutions for NLP or LP problem because of the short-circuit mechanism. #3855

@Toflamus

Description

@Toflamus

Summary

I ran gdpopt.ldsda using mindtpy as the MINLP subproblem solver. The algorithm returned a results object that contains no solutions.

This appears related to MindtPy’s short-circuit (e.g., all discrete variables are fixed or just an NLP problem), where MindtPy may directly solve an LP/NLP but does not reliably return a populated SolverResults / does not load primal values back onto the model instance provided to solve().

Steps to reproduce the issue

Reproducer A (direct MindtPy; “no discrete decisions” path)
Run the following script:

# example.py
  from pyomo.environ import ConcreteModel, Var, Objective, Constraint, Binary, NonNegativeReals, value, SolverFactory
  
  m = ConcreteModel()
  m.x = Var(domain=NonNegativeReals)
  m.y = Var(domain=Binary)
  
  # Fix the discrete variable so MindtPy detects "no discrete decisions"
  m.y.fix(0)
  
  # NLP constraint so MindtPy takes the NLP direct-solve branch
  m.c = Constraint(expr=m.x**2 >= 1 + m.y)
  m.obj = Objective(expr=m.x)
  
  res = SolverFactory("mindtpy").solve(
      m,
      strategy="OA",
      nlp_solver="ipopt",
      # mip_solver can be provided but should not matter if there are no discretes
      mip_solver="appsi_highs",
  )
  
  print("res is None:", res is None)

Observed in the test output:
res is None: True

Expected behavior
res is None: False

Reproducer B (GDPopt LD-SDA gams free test)

Please change the pyomo/contrib/gdpopt/test/test_ldsda.py to:

class TestGDPoptLDSDA(unittest.TestCase):

....notations...

    @unittest.skipUnless(
        all(
            (
                SolverFactory("mindtpy").available(False),
                SolverFactory("appsi_highs").available(False),
                SolverFactory("ipopt").available(False),
            )
        ),
        "mindtpy/appsi_highs/ipopt not available",
    )
    def test_solve_four_stage_dynamic_model_minimize(self):
    
        model = build_model(mode_transfer=True)
        
        discretizer = TransformationFactory("dae.collocation")
        discretizer.apply_to(model, nfe=10, ncp=3, scheme="LAGRANGE-RADAU")
      
        for disjunct in model.component_data_objects(ctype=Disjunct):
            for constraint in disjunct.component_objects(ctype=Constraint):
                constraint._constructed = False
                constraint.construct()

        for dxdt in model.component_data_objects(ctype=Var, descend_into=True):
            if "dxdt" in dxdt.name:
                dxdt.setlb(-300)
                dxdt.setub(300)

        for direction_norm in ["L2", "Linf"]:
            results = SolverFactory("gdpopt.ldsda").solve(
                model,
                direction_norm=direction_norm,
                minlp_solver="mindtpy",
                minlp_solver_args={"mip_solver": "appsi_highs", "nlp_solver": "ipopt"},
                # minlp_solver="gams",
                # minlp_solver_args={'solver': "ipopt"},
                starting_point=[1, 2],
                logical_constraint_list=[
                    model.mode_transfer_lc1,
                    model.mode_transfer_lc2,
                ],
                time_limit=100,
            )
        
            self.assertAlmostEqual(value(model.obj), -23.305325, places=4)

...the rest of test unchanged...
pytest <path_to_test.py>

Error Message

The error message

============================================= test session starts =============================================
platform win32 -- Python 3.12.12, pytest-8.4.2, pluggy-1.5.0
rootdir: 
configfile: pyproject.toml
plugins: cov-7.0.0
collected 10 items

pyomo\contrib\gdpopt\tests\test_ldsda.py F.........                                                      [100%]

================================================== FAILURES ===================================================
________________________ TestGDPoptLDSDA.test_solve_four_stage_dynamic_model_minimize _________________________

self = <pyomo.contrib.gdpopt.tests.test_ldsda.TestGDPoptLDSDA testMethod=test_solve_four_stage_dynamic_model_minimize>

    @unittest.skipUnless(
        all(
            (
                SolverFactory("mindtpy").available(False),
                SolverFactory("appsi_highs").available(False),
                SolverFactory("ipopt").available(False),
            )
        ),
        "mindtpy/appsi_highs/ipopt not available",
    )
    def test_solve_four_stage_dynamic_model_minimize(self):

        model = build_model(mode_transfer=True)

        discretizer = TransformationFactory("dae.collocation")
        discretizer.apply_to(model, nfe=10, ncp=3, scheme="LAGRANGE-RADAU")

        for disjunct in model.component_data_objects(ctype=Disjunct):
            for constraint in disjunct.component_objects(ctype=Constraint):
                constraint._constructed = False
                constraint.construct()

        for dxdt in model.component_data_objects(ctype=Var, descend_into=True):
            if "dxdt" in dxdt.name:
                dxdt.setlb(-300)
                dxdt.setub(300)

        for direction_norm in ["L2", "Linf"]:
            results = SolverFactory("gdpopt.ldsda").solve(
                model,
                direction_norm=direction_norm,
                minlp_solver="mindtpy",
                minlp_solver_args={"mip_solver": "appsi_highs", "nlp_solver": "ipopt"},
                # minlp_solver="gams",
                # minlp_solver_args={'solver': "ipopt"},
                starting_point=[1, 2],
                logical_constraint_list=[
                    model.mode_transfer_lc1,
                    model.mode_transfer_lc2,
                ],
                time_limit=100,
            )

>           self.assertAlmostEqual(value(model.obj), -23.305325, places=4)
                                   ^^^^^^^^^^^^^^^^

pyomo\contrib\gdpopt\tests\test_ldsda.py:75:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
pyomo\common\numeric_types.py:378: in value
    tmp = obj(exception=True)
          ^^^^^^^^^^^^^^^^^^^
pyomo\core\base\objective.py:462: in __call__
    return super().__call__(exception)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
pyomo\core\base\expression.py:63: in __call__
    return arg(exception=exception)
           ^^^^^^^^^^^^^^^^^^^^^^^^
pyomo\core\expr\base.py:116: in __call__
    return visitor.evaluate_expression(self, exception)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pyomo\core\expr\visitor.py:1312: in evaluate_expression
    ans = visitor.dfs_postorder_stack(exp)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pyomo\core\expr\visitor.py:930: in dfs_postorder_stack
    flag, value = self.visiting_potential_leaf(_sub)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pyomo\core\expr\visitor.py:1213: in visiting_potential_leaf
    return True, value(node, exception=self.exception)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

obj = <pyomo.core.base.var.VarData object at 0x000002E32D106F90>, exception = True

    def value(obj, exception=True):
        """
        A utility function that returns the value of a Pyomo object or
        expression.

        Args:
            obj: The argument to evaluate. If it is None, a
                string, or any other primitive numeric type,
                then this function simply returns the argument.
                Otherwise, if the argument is a NumericValue
                then the __call__ method is executed.
            exception (bool): If :const:`True`, then an exception should
                be raised when instances of NumericValue fail to
                evaluate due to one or more objects not being
                initialized to a numeric value (e.g, one or more
                variables in an algebraic expression having the
                value None). If :const:`False`, then the function
                returns :const:`None` when an exception occurs.
                Default is True.

        Returns: A numeric value or None.
        """
        if obj.__class__ in native_types:
            return obj
        #
        # Test if we have a duck typed Pyomo expression
        #
        if not hasattr(obj, 'is_numeric_type'):
            #
            # TODO: Historically we checked for new *numeric* types and
            # raised exceptions for anything else.  That is inconsistent
            # with allowing native_types like None/str/bool to be returned
            # from value().  We should revisit if that is worthwhile to do
            # here.
            #
            if check_if_numeric_type(obj):
                return obj
            else:
                if not exception:
                    return None
                raise TypeError(
                    "Cannot evaluate object with unknown type: %s" % obj.__class__.__name__
                )
        #
        # Evaluate the expression object
        #
        if exception:
            #
            # Here, we try to catch the exception
            #
            try:
                tmp = obj(exception=True)
                if tmp is None:
>                   raise ValueError(
                        "No value for uninitialized %s object %s"
                        % (type(obj).__name__, obj.name)
                    )
E                   ValueError: No value for uninitialized VarData object x1[0.015505]

pyomo\common\numeric_types.py:380: ValueError
---------------------------------------------- Captured log call ----------------------------------------------
ERROR    pyomo.common.numeric_types:numeric_types.py:391 evaluating object as numeric value: x1[0.015505]
    (object: <class 'pyomo.core.base.var.VarData'>)
No value for uninitialized VarData object x1[0.015505]
ERROR    pyomo.common.numeric_types:numeric_types.py:391 evaluating object as numeric value: obj
    (object: <class 'pyomo.core.base.objective.ScalarObjective'>)
No value for uninitialized VarData object x1[0.015505]
=========================================== short test summary info ===========================================
FAILED pyomo/contrib/gdpopt/tests/test_ldsda.py::TestGDPoptLDSDA::test_solve_four_stage_dynamic_model_minimize - ValueError: No value for uninitialized VarData object x1[0.015505]
========================================= 1 failed, 9 passed in 3.90s =========================================

Information on your system

Pyomo version: Pyomo 6.10.0.dev0
Python version: CPython 3.12.12
Operating system: Windows 11
How Pyomo was installed (PyPI, conda, source): Git clone, pip install -e
Solver (if applicable): MIP: appsi_highs & NLP: ipopt

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions