Skip to content

Fix gdp_col hull reformulation - #124

Open
bernalde wants to merge 2 commits into
mainfrom
fix/issue-66-gdp-col-hull
Open

bernalde wants to merge 2 commits into
mainfrom
fix/issue-66-gdp-col-hull

Conversation

@bernalde

@bernalde bernalde commented May 11, 2026

Copy link
Copy Markdown
Member

Summary

  • Initialize gdp_col feed liquid and vapor enthalpy variables from the existing feed enthalpy expressions so construction no longer emits out-of-bounds Pyomo W1002 warnings.
  • Rewrite the active-tray Antoine vapor-pressure relation in algebraically equivalent exponential form so Pyomo's default gdp.hull transformation no longer evaluates log(0) in the hull perspective.
  • Add focused regression coverage for feed enthalpy initialization and solver-free gdp.hull reformulation.
  • Add an optional Pixi gurobi environment for direct gurobipy/Pyomo Gurobi checks without making licensed solver bindings part of the default environment.

Tests run

  • /home/bernalde/.pixi/bin/pixi run pytest tests/test_pyomo_deprecations.py -v --tb=short -> 6 passed.
  • /home/bernalde/.pixi/bin/pixi run pytest tests/test_module_imports.py -v --tb=short -> 68 passed.
  • /home/bernalde/.pixi/bin/pixi run test -> 279 passed, 1 skipped.
  • /home/bernalde/.pixi/bin/pixi run lint -> passed with exit code 0. The configured non-fatal flake8 inventory still reports existing repository style findings under --exit-zero.
  • /home/bernalde/.pixi/bin/pixi run gdplib-benchmark warnings --instances gdp_col --strategies gdp.hull --mode transform --no-check-solvers --run-id issue66_hull_warning --metadata-dir /tmp/gdplib-issue66-warning-metadata -> 0 warning events, 0 unique warning rows, 0 deprecation candidate events, 0 capture errors.
  • /home/bernalde/.pixi/bin/pixi install -e gurobi -> installed the optional Gurobi Pixi environment.
  • /home/bernalde/.pixi/bin/pixi run -e gurobi python -c "import gurobipy as gp; from pyomo.environ import SolverFactory; print(gp.gurobi.version()); print(SolverFactory('gurobi_direct').available(False))" -> (12, 0, 3), True.
  • /home/bernalde/.pixi/bin/pixi run -e gurobi pytest tests/test_release_workflow.py -v --tb=short -> 5 passed.
  • Direct Pyomo/Gurobi smoke solve through gurobi_direct -> ok, optimal, objective 4.0.
  • git diff --check -> passed.

Solver-backed benchmark evidence

The benchmark comparisons now include GAMS/Gurobi cases for all direct GDP reformulations in this PR scope (gdp.bigm and gdp.hull), in addition to the local DICOPT and global BARON profiles. The GAMS/Gurobi rows below were run through the benchmark CLI with --solver-profile gams-gurobi, so they use the same single-threaded GAMS options as the other campaign rows.

Commands:

/home/bernalde/.pixi/bin/pixi run gdplib-benchmark run --instances gdp_col --strategies gdp.hull gdpopt.gloa gdpopt.lbb --timelimit 300 --solver-profile gams-local --run-id issue66_local_5min_20260511
/home/bernalde/.pixi/bin/pixi run gdplib-benchmark run --instances gdp_col --strategies gdp.hull gdpopt.gloa gdpopt.lbb --timelimit 300 --solver-profile gams-baron --run-id issue66_global_5min_20260511
/home/bernalde/.pixi/bin/pixi run -e gurobi gdplib-benchmark run --instances gdp_col --strategies gdp.bigm gdp.hull --timelimit 300 --solver-profile gams-gurobi --run-id issue66_gurobi_5min_20260511 --no-summary
Profile Strategy Solver Termination Status Objective Lower bound Upper bound User time (s)
gams-local gdp.hull DICOPT intermediateNonInteger warning 10273.524906 13110.109796 10273.524906 3.516
gams-gurobi gdp.bigm Gurobi maxTimeLimit ok 11014.509755 300.289
gams-gurobi gdp.hull Gurobi feasible ok 22355.240680 19223.858427 22355.240680 300.047
gams-baron gdp.hull BARON infeasible ok 1000.000000 0.423
gams-local gdpopt.gloa DICOPT/IPOPTH/Gurobi roles optimal ok 20916.392216 20916.392216 20916.392216 29.078
gams-baron gdpopt.gloa BARON roles maxTimeLimit ok 22355.240679 8000.000000 22355.240679 302.049
gams-local gdpopt.lbb DICOPT/IPOPTH/Gurobi roles failure error
gams-baron gdpopt.lbb BARON roles failure error

Additional Gurobi checks:

  • GAMS/Gurobi accepts both direct reformulations as nonconvex MINLPs.
  • For gdp.hull, GAMS/Gurobi finds a feasible incumbent at objective 22355.24067954157 within 300 seconds.
  • For gdp.bigm, Gurobi reaches the 300-second time limit with no incumbent (Solution count 0, Best objective -) and a lower bound of 11014.509754975745. The GAMS wrapper reports OBJVAL/upper bound as 17000.0, but the Gurobi log does not support treating that as a valid incumbent, so the table leaves objective and upper bound blank.
  • Pyomo's direct Gurobi interfaces are available in the optional Pixi environment, but they cannot currently write this nonlinear transformed hull instance directly: gurobi rejects nonlinear LP-format output, while gurobi_direct and appsi_gurobi reject expression degree None.
  • Future solver-backed benchmark comparison tables should include the gams-gurobi profile for each direct reformulation alongside gams-local and gams-baron when Gurobi is licensed locally.

Notes

  • gdp.hull now transforms cleanly, but direct DICOPT and BARON behavior remains solver/formulation sensitive: DICOPT stops with intermediateNonInteger, while BARON reports the transformed hull model infeasible despite GAMS/Gurobi finding a feasible incumbent.
  • gdpopt.lbb remains blocked by Pyomo GDPopt runtime behavior: GDP_LBB_Solver calls stale _get_final_results_object() in the time-limit path. I opened upstream Pyomo issue GDPopt LBB time-limit path calls stale _get_final_results_object Pyomo/pyomo#3941 with a minimal reproducer.
  • Generated benchmark artifacts were not committed. Per-case JSON/log files from the local checks remain under ignored benchmark output directories.

Closes #66

@bernalde

Copy link
Copy Markdown
Member Author

Local solver campaign update

I ran bounded 5-minute solver campaigns for gdp_col on this branch to check the remaining issue #66 strategy surface after the hull transformation fix.

Commands:

/home/bernalde/.pixi/bin/pixi run gdplib-benchmark preflight --instances gdp_col --strategies gdp.hull gdpopt.gloa gdpopt.lbb --timelimit 300 --solver-profile gams-local --build-models --check-solvers
/home/bernalde/.pixi/bin/pixi run gdplib-benchmark run --instances gdp_col --strategies gdp.hull gdpopt.gloa gdpopt.lbb --timelimit 300 --solver-profile gams-local --run-id issue66_local_5min_20260511
/home/bernalde/.pixi/bin/pixi run gdplib-benchmark preflight --instances gdp_col --strategies gdp.hull gdpopt.gloa gdpopt.lbb --timelimit 300 --solver-profile gams-baron --build-models --check-solvers
/home/bernalde/.pixi/bin/pixi run gdplib-benchmark run --instances gdp_col --strategies gdp.hull gdpopt.gloa gdpopt.lbb --timelimit 300 --solver-profile gams-baron --run-id issue66_global_5min_20260511

Environment metadata:

  • Python 3.12.13
  • Pyomo 6.10.0
  • GAMS executable: /home/bernalde/packages/gams51.2_linux_x64_64_sfx/gams
  • Git head: 7c47e62039baa354cbcfa0f7846500dcc84f8faa

Results:

Profile Strategy Solver Termination Status Objective Lower bound Upper bound User time (s)
gams-local gdp.hull DICOPT intermediateNonInteger warning 10273.524906 13110.109796 10273.524906 3.516
gams-local gdpopt.gloa DICOPT/IPOPTH/Gurobi roles optimal ok 20916.392216 20916.392216 20916.392216 29.078
gams-local gdpopt.lbb DICOPT/IPOPTH/Gurobi roles failure error
gams-baron gdp.hull BARON infeasible ok 1000.000000 0.423
gams-baron gdpopt.gloa BARON roles maxTimeLimit ok 22355.240679 8000.000000 22355.240679 302.049
gams-baron gdpopt.lbb BARON roles failure error

Failure details:

  • Both LBB runs fail with the same Pyomo GDPopt exception: AttributeError: 'GDP_LBB_Solver' object has no attribute '_get_final_results_object'. Did you mean: '_get_final_pyomo_results_object'?
  • Failure logs:
    • gdplib/gdp_col/benchmark_result/issue66_local_5min_20260511/gdpopt.lbb_gams_dicopt_failure.log
    • gdplib/gdp_col/benchmark_result/issue66_global_5min_20260511/gdpopt.lbb_gams_baron_failure.log

Runner note:

  • Solver calls completed, but gdplib-benchmark run exits during summary generation because _generate_summary() imports missing module generate_benchmark_summary_all. Per-case JSON/CSV artifacts were still written under benchmark_runs/issue66_* and gdplib/gdp_col/benchmark_result/issue66_*.

Interpretation:

  • The PR fix moves gdp.hull past the previous transformation-time math domain error.
  • gdpopt.gloa with the local profile now solves to an optimal local result in this bounded run.
  • Direct hull solve quality is still not clean: local DICOPT stops intermediateNonInteger, and BARON reports the transformed hull model infeasible.
  • LBB remains blocked by a GDPopt runtime error that appears independent of the gdp_col formulation.

@bernalde

Copy link
Copy Markdown
Member Author

Follow-up: GDPopt LBB failure root cause

I dug further into the gdpopt.lbb failure reported in the local solver campaign.

Root cause:

  • This is a Pyomo GDPopt LBB time-limit exit bug, not a gdp_col formulation failure.
  • In Pyomo 6.10.0, pyomo/contrib/gdpopt/branch_and_bound.py::GDP_LBB_Solver._solve_gdp() calls self._get_final_results_object() only in the reached_time_limit(config) branch.
  • The base GDPopt algorithm class no longer provides _get_final_results_object; it provides _get_final_pyomo_results_object().
  • The same stale call is present on upstream Pyomo main as of this check.

Evidence:

  • The installed class has hasattr(GDP_LBB_Solver, "_get_final_results_object") == False and hasattr(GDP_LBB_Solver, "_get_final_pyomo_results_object") == True.
  • A minimal GDP model reproduces the same AttributeError by forcing the LBB time-limit branch, without using gdp_col or any GAMS solve.
  • A local monkeypatch aliasing _get_final_results_object = _get_final_pyomo_results_object lets the time-limit path return a normal Pyomo results object.

What the monkeypatch shows for gdp_col:

  • With a 30-second local-profile LBB run, the patched solver returns maxTimeLimit, no feasible incumbent, and infinite bounds.
  • So the method-name fix would make benchmark reporting cleaner, but it does not make LBB solve gdp_col.

Related upstream context:

Practical implication for this PR:

  • gdpopt.lbb should remain tracked as blocked by Pyomo LBB runtime behavior, not by this gdp_col hull transformation fix.
  • A separate follow-up could either report/fix the Pyomo method-name bug upstream or add a temporary compatibility shim in gdplib-benchmark if maintainers want local campaign reports to classify LBB as maxTimeLimit instead of a Python exception.

@bernalde
bernalde force-pushed the fix/issue-66-gdp-col-hull branch from db9de24 to e1018a6 Compare May 14, 2026 16:19

@bernalde bernalde left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintainer review of head e1018a6.

Both changes are correct, verified line by line. The feed-enthalpy initializers now come from the existing feed_liq_enthalpy_expr/feed_vap_enthalpy_expr Expressions (well-defined at construction; hand-computed values ≈10.1 and ≈34 land inside bounds, eliminating the W1002 warnings). The Antoine/Wagner rewrite from log form to exp form is algebraically equivalent given Pvap in [1e-3, 5] > 0 and Pvap_X in [0.25, 0.5], and under hull disaggregation the exp form stays evaluable at the origin where the base log(Pvap) failed; the constraint is valid in both its disjunct and global-block contexts, and the involved disjunct variables are globally bounded as hull requires.

Findings:

Blocking: closes an unresolved tracker — the body says "Closes #66", but issue #66's checklist has three open items (Hull, GLOA, LBB) and this PR's own benchmark table shows gdpopt.lbb still failing on both profiles (upstream Pyomo/pyomo#3941) and BARON declaring the hull-transformed model infeasible. Merging as-is auto-closes a tracker that is not resolved. Fix: treat the link as Refs #66 (recorded in a PR comment since the body stands), or close #66 only alongside a follow-up owning LBB and the BARON discrepancy.

Nonblocking: stale body bullet — the body's "Add an optional Pixi gurobi environment" is stale: [feature.gurobi] already exists at the merge-base (via PR #144); this diff touches only two files. The corrected scope should be recorded so reviewers are not misled.

Nonblocking: test placement — test_gdp_col_reformulates_with_hull and its sibling are not deprecation tests; placing them in tests/test_pyomo_deprecations.py is mild scope creep. Move both to a new tests/test_gdp_col.py.

Question: BARON infeasibility — BARON declares the hull-transformed model infeasible in 0.42 s while Gurobi finds a 22355.24 incumbent on the same model. Is the exp rewrite triggering a BARON domain/presolve issue worth a tracked follow-up on #66?

Evidence note: the gams-local gdpopt.gloa optimal 20916.392216 row (LB = UB) is the strongest solver evidence and supports the GLOA fix; the hull rows prove hull transforms and is solvable in principle, not that it is robustly fixed.

Summary — Blocking: 1. Nonblocking: 2. Questions: 1. Tests: static line-by-line verification of both fixes, including bound/domain arithmetic; CI is green at this head (6/6 checks). I would not merge this until the blocking issue above is addressed (an issue-linkage record; the code itself is sound). Posting as COMMENT (author account); the main ruleset requires one approving review from another maintainer.

@bernalde

Copy link
Copy Markdown
Member Author

Addressed the review of head e1018a6 in commit a1c16ec:

  • test-placement: the two gdp_col regression tests moved from tests/test_pyomo_deprecations.py to a new tests/test_gdp_col.py; the deprecation module is back to deprecation-only scope.
  • closes-vs-refs-66 (Blocking, metadata): recorded here since the body stands — this PR partially resolves issue Refactor gdp_col to fix benchmark issues #66 (hull transformation fixed with a test; local GLOA optimal at 20916.392216), while gdpopt.lbb remains blocked upstream (GDPopt LBB time-limit path calls stale _get_final_results_object Pyomo/pyomo#3941) and the BARON-infeasibility discrepancy is unresolved, so the link should be read as Refs #66. If merged as-is, the auto-closure of Refactor gdp_col to fix benchmark issues #66 should be reverted (reopen) or explicitly accepted with the remaining items tracked; a coordination note is posted on Refactor gdp_col to fix benchmark issues #66.
  • stale-gurobi-bullet: corrected scope for the record — the optional Pixi [feature.gurobi] environment already existed at the merge-base (PR [codex] Consolidate benchmark support changes #144); this PR's diff is exactly two files (gdplib/gdp_col/column.py, tests), and body bullets referring to the gurobi environment describe pre-existing state, not changes made here.
  • baron-infeasible-followup (question): tracked on Refactor gdp_col to fix benchmark issues #66 via the coordination note — BARON declares the hull-transformed model infeasible in 0.42 s while Gurobi finds a 22355.24 incumbent on the same model; worth investigating whether the exp-form rewrite trips a BARON presolve/domain issue.

Tests: pixi run pytest tests/test_gdp_col.py tests/test_pyomo_deprecations.py — 6 passed; black --check clean. CI for the new head was starting when this was posted; the previous head was green (6/6).

@bernalde

Copy link
Copy Markdown
Member Author

Follow-up on the BARON-infeasibility question from the review: it does not reproduce at this head. Hull + GAMS/BARON (120 s, optcr=1e-6, threads=1) returns the 22355.24 incumbent with max constraint violation 4.1e-9, and Big-M + BARON returns a feasible 19449.85 — which, for this minimize model, is strictly better than the body's "GLOA optimal 20916.39", so that gams-local GLOA label is falsified as a global claim. Details and re-baselining follow-ups recorded on #66.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor gdp_col to fix benchmark issues

1 participant