Skip to content

GDPopt LBB routes continuous node subproblems through minlp_solver #3945

Description

@bernalde

Summary

GDPopt LBB currently sends relaxed node subproblems to minlp_solver even when the transformed node subproblem has no unfixed discrete variables.

This can route continuous NLP subproblems to MINLP-only solver choices. In a GAMS workflow, that can fail before solve with messages like:

ValueError: GAMS writer passed solver (dicopt) unsuitable for model type (nlp)

The inverse workaround is also not safe: forcing minlp_solver to an NLP solver can fail on other LBB nodes that are still MINLP:

ValueError: GAMS writer passed solver (ipopth) unsuitable for model type (minlp)

It seems LBB needs model-class-aware dispatch for node subproblems, or a clearly documented solver role for "relaxed node solver" that can handle both continuous and mixed-integer node formulations.

Minimal working example

This MWE does not require any external solver. It monkeypatches the SolverFactory used inside pyomo.contrib.gdpopt.branch_and_bound to record which solver role LBB calls and whether the subproblem still contains unfixed discrete variables.

import pyomo.environ as pyo
import pyomo.contrib.gdpopt.branch_and_bound as bb
from pyomo.contrib.gdpopt.branch_and_bound import GDP_LBB_Solver
from pyomo.contrib.gdpopt.create_oa_subproblems import (
    add_algebraic_variable_list,
    add_util_block,
)
from pyomo.opt import SolverResults, SolverStatus, TerminationCondition


class RecordingSolver:
    def __init__(self, name):
        self.name = name

    def solve(self, model, **kwds):
        has_discrete = any(
            (v.is_binary() or v.is_integer()) and not v.fixed
            for v in model.component_data_objects(pyo.Var, descend_into=True)
        )
        print(f"called solver role: {self.name}")
        print(f"subproblem has unfixed discrete variables: {has_discrete}")

        results = SolverResults()
        results.solver.status = SolverStatus.ok
        results.solver.termination_condition = TerminationCondition.optimal
        results.problem.lower_bound = 0
        results.problem.upper_bound = 0
        return results


def recording_solver_factory(name):
    return RecordingSolver(name)


original_solver_factory = bb.SolverFactory
bb.SolverFactory = recording_solver_factory

try:
    m = pyo.ConcreteModel()
    m.x = pyo.Var(bounds=(0, 1))
    m.obj = pyo.Objective(expr=m.x)

    solver = GDP_LBB_Solver()
    solver.pyomo_results = SolverResults()
    solver.pyomo_results.problem.sense = pyo.minimize
    solver.original_util_block = add_util_block(m)
    add_algebraic_variable_list(solver.original_util_block)

    config = solver.CONFIG()
    config.minlp_solver = "sentinel_minlp"
    config.nlp_solver = "sentinel_nlp"
    config.minlp_solver_args = {}
    config.integer_tolerance = 1e-5
    config.time_limit = None

    solver._solve_rnGDP_subproblem(m, config)
finally:
    bb.SolverFactory = original_solver_factory

Observed output:

called solver role: sentinel_minlp
subproblem has unfixed discrete variables: False

This isolates the dispatch behavior in _solve_rnGDP_subproblem(): even with a continuous subproblem, LBB calls config.minlp_solver.

Expected behavior

If the transformed LBB node subproblem has no unfixed binary/integer variables, GDPopt should not blindly route it through the minlp_solver role.

A reasonable dispatch policy could be:

has_unfixed_discrete = any(
    (v.is_binary() or v.is_integer()) and not v.fixed
    for v in subproblem.component_data_objects(Var, descend_into=True)
)

if has_unfixed_discrete:
    use minlp_solver / minlp_solver_args
else:
    use a continuous NLP-capable solver role

There is a global-optimization caveat: for nonconvex continuous NLP node subproblems, using a local NLP solver such as IPOPT may not provide a rigorous LBB bound. One possible design is a separate relaxed_nlp_solver or global_nlp_solver role, defaulting conservatively to the existing minlp_solver for global correctness, but allowing users to route continuous node subproblems to an appropriate solver explicitly.

Downstream context

This surfaced while benchmarking GDPLib's HDA model:

Observed HDA evidence:

  • gdpopt.lbb with the GDPLib gams-local profile routes minlp_solver to GAMS/DICOPT.
  • During LBB branching, a fully branched continuous node reaches GAMS as an nlp, and the GAMS writer rejects DICOPT for that problem class.
  • Overriding minlp_solver to GAMS/IPOPTH avoids that specific error, but another node reaches GAMS as a minlp, and the GAMS writer rejects IPOPTH.
  • GAMS/Gurobi and GAMS/BARON profiles get farther because those solvers can cover more of the node problem classes, but long runs then hit the separate LBB time-limit result bug already reported in GDPopt LBB time-limit path calls stale _get_final_results_object #3941.

Related downstream evidence from earlier GDPLib LBB work:

This issue is distinct from, but often masked by, the LBB time-limit stale-method bug in #3941.

Environment

Observed locally with:

Python 3.12.13
Pyomo 6.10.0

I also checked pyomo/contrib/gdpopt/branch_and_bound.py on upstream Pyomo/pyomo main; _solve_rnGDP_subproblem() still appears to call config.minlp_solver unconditionally for the transformed node subproblem.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions