Skip to content

GDPopt LBB routes continuous node subproblems through minlp_solver #3945

@bernalde

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions