Skip to content

Track GDPopt LOA objective-sense discrepancy #114

@bernalde

Description

@bernalde

Summary

While working on #42 / PR #113, the methanol benchmark exposed a GDPopt LOA objective-sense discrepancy. Solving the same GDP model as minimize(-profit) and maximize(profit) should be mathematically equivalent, but GDPopt returns different local solutions and terminates much earlier on the maximization form.

This is probably not part of PR #113, because that PR should keep the currently verified minimize(-profit) formulation. This issue tracks the behavior so we can reduce it to a smaller Pyomo reproducer and, if confirmed, report/fix it upstream in Pyomo.

Related:

Local reproducer

Environment:

  • GDPlib branch: fix/issue-42-methanol-gdpopt
  • commit tested: 840671515c70a722f5e67fc8eb7bc921bf6d28ef
  • Pyomo: 6.10.0 from the committed Pixi environment
  • solver path: SolverFactory("gdpopt").solve(..., algorithm="LOA", mip_solver="gams", nlp_solver="gams")
  • local GAMS interface was available

Script:

import pyomo.environ as pyo
import pyomo.gdp as gdp
from gdplib.methanol.methanol import build_model


def solve_case(label, maximize_profit):
    m = build_model()
    if maximize_profit:
        m.objective.deactivate()
        m.max_profit_objective = pyo.Objective(expr=m.profit, sense=pyo.maximize)
    res = pyo.SolverFactory("gdpopt").solve(
        m,
        algorithm="LOA",
        mip_solver="gams",
        nlp_solver="gams",
        tee=False,
    )
    active = [
        d.name
        for d in m.component_data_objects(
            gdp.Disjunct, active=True, sort=True, descend_into=True
        )
        if pyo.value(d.indicator_var)
    ]
    print(f"CASE: {label}")
    print(f"termination: {res.solver.termination_condition}")
    print(f"objective: {pyo.value(m.objective) if m.objective.active else pyo.value(m.max_profit_objective)}")
    print(f"profit: {pyo.value(m.profit)}")
    print(f"iterations: {getattr(res.solver, 'iterations', None)}")
    print(f"active: {active}")
    print(f"feed1: {pyo.value(m.flows[1])}")
    print(f"feed2: {pyo.value(m.flows[2])}")
    print(f"product_flow: {pyo.value(m.flows[23])}")
    print(f"purity: {pyo.value(m.component_flows[23, 'CH3OH'] / m.flows[23])}")
    print("---")


solve_case("minimize_negative_profit", maximize_profit=False)
solve_case("maximize_profit", maximize_profit=True)

Observed output:

CASE: minimize_negative_profit
termination: optimal
objective: -1793.4292381783353
profit: 1793.4292381783353
iterations: 15
active: ['cheap_reactor', 'expensive_feed_disjunct', 'single_stage_recycle_compressor_disjunct', 'two_stage_feed_compressor_disjunct']
feed1: 0
feed2: 3.408977431099573
product_flow: 1.0
purity: 0.9
---
CASE: maximize_profit
termination: optimal
objective: -242.05445652916296
profit: -242.05445652916296
iterations: 1
active: ['cheap_feed_disjunct', 'cheap_reactor', 'two_stage_feed_compressor_disjunct', 'two_stage_recycle_compressor_disjunct']
feed1: 4.820913073983858
feed2: 0
product_flow: 1.0
purity: 0.9
---

Why this is suspicious

The two formulations are algebraically equivalent. For a local GDP/MINLP method, different starts or linearizations can lead to different local solutions, but simply expressing the same objective as maximize(profit) instead of minimize(-profit) should not make the discrete master pursue the opposite economic direction or terminate after one iteration with a dominated solution.

The LOA paper reports Example 3 optimal profit around $1.794M/yr, and the minimize(-profit) form reproduces this scale. The maximize(profit) form returns a negative-profit flowsheet.

Initial Pyomo source hypothesis

In Pyomo 6.10.0, pyomo.contrib.gdpopt.loa.GDP_LOA_Solver._setup_augmented_penalty_objective() deactivates the discrete problem objective and creates:

discrete_problem_util_block.oa_obj = Objective(sense=minimize)

This appears unconditional. _update_augmented_penalty_objective() adjusts the slack penalty sign for maximize/minimize, but it assigns:

discrete_problem_util_block.oa_obj.expr = (
    discrete_objective.expr + OA_penalty_expr
)

without changing oa_obj.sense to match the original discrete objective. The rest of GDPopt does have objective-sense-aware bound updates, so this looks like a possible discrete master objective sense bug rather than a purely expected nonconvex-local-solver artifact.

This hypothesis still needs a smaller standalone reproducer before filing upstream.

Acceptance criteria

  • Build a minimal GDP/GDPopt reproducer where minimize(-f) and maximize(f) diverge under LOA.
  • Confirm whether the unconditional minimization objective in GDPopt LOA is the root cause.
  • If confirmed, open a Pyomo issue or PR with the minimal reproducer and candidate fix.
  • After the upstream status is clear, decide whether GDPlib should keep the workaround, add an xfail regression, or update the methanol benchmark solve path.

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