Skip to content

Fix stranded gas learning reformulation - #148

Open
bernalde wants to merge 2 commits into
mainfrom
fix/issue-76-stranded-gas-learning
Open

bernalde wants to merge 2 commits into
mainfrom
fix/issue-76-stranded-gas-learning

Conversation

@bernalde

Copy link
Copy Markdown
Member

Summary

  • Replaced the stranded_gas module-learning logarithm inside the module-existence disjunct with a finite purchase-count selector over the existing 0..5 purchase limit.
  • Made absent module types force zero purchases, and marked exactly-one stranded_gas disjunctions with xor=True.
  • Added focused stranded_gas semantic and gdp.bigm/gdp.hull transformation tests, then regenerated model-size reports.

Tests run

  • /home/bernalde/.pixi/bin/pixi run pytest tests/test_stranded_gas.py tests/test_module_imports.py -v --tb=short - passed, 86 passed in 48.52s.
  • /home/bernalde/.pixi/bin/pixi run test - passed, 340 passed and 1 skipped in 93.33s.
  • /home/bernalde/.pixi/bin/pixi run lint - passed. Black and critical flake8 passed; the repository's --exit-zero flake8 inventory still reports existing style issues outside this PR.
  • /home/bernalde/.pixi/bin/pixi run pytest tests/test_stranded_gas.py -v --tb=short - passed after formatting, 12 passed in 33.98s.
  • git diff --check - passed.
  • /home/bernalde/.pixi/bin/pixi run gdplib-benchmark preflight --instances stranded_gas --strategies gdp.bigm gdp.hull --solver-profile gams-baron --timelimit 60 --build-models --check-solvers - passed.
  • /home/bernalde/.pixi/bin/pixi run gdplib-benchmark run --instances stranded_gas --strategies gdp.bigm --solver-profile gams-baron --timelimit 60 --run-id issue76_bigm_baron_60s --fail-fast --no-summary - passed with 1 result row and 0 failures; BARON reached maxTimeLimit with a finite lower bound instead of the previous infeasible result.

Notes

Closes #76

@bernalde

Copy link
Copy Markdown
Member Author

Objective-sense note (context: Pyomo/pyomo#3986, merged 2026-08-13, fixed the GDPopt LOA objective-sense bug tracked in #114):

This PR deliberately keeps the minimize(-profit) convention (m.neg_profit), and that is the right call for now — the LOA fix exists only on Pyomo main, so a native maximize(profit) objective would make LOA correctness depend on unreleased Pyomo, while the negated form is correct on every version. The verification here (Big-M/Hull transforms, BARON) is unaffected by the bug either way.

Recommendation: merge this PR as-is; revert to the natural maximize objective in a follow-up pass (together with methanol, see #76 / #114) once a Pyomo release containing the fix ships and GDPLib can require it.

@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 9011803.

The reformulation is exact on the feasible set. limit_module_purchases caps total purchases across all types at 5, so per-type totals lie in {0..5}; the tabulated learning_factor_by_purchase_count matches the closed form (1-LR)^(log N / log 2) at every feasible integer, and count 0 is excluded inside mtype_exists by require_module_purchases + match_purchase_count, so the log(0) singularity that broke transformations is gone. Removing the manual m.BigM annotation is safe now that learning_factor_calc is linear. The size-report deltas check out arithmetically (+6 binaries and +3 constraints per module type; nonlinear constraints to 0 — the remaining exp/sqrt uses are Param-value computations, not constraints).

Findings:

Nonblocking: undocumented tightening — no_module_purchases in mtype_absent is a genuine restriction relative to base, where purchases of an "absent" type were still feasible. No optimum is lost (the exists branch weakly dominates: learning_factor <= 1 only lowers module_unit_cost and profit is maximized), but that dominance argument lives nowhere in the code. Add one sentence to the mtype_absent docstring noting the tightening and why it preserves optima.

Nonblocking: xor no-op wording — xor=True is Pyomo's Disjunction default, so the three edits are clarifications, not behavior changes; the body's "marked exactly-one" phrasing implies otherwise. Keep the explicitness; correct the description in a follow-up comment.

Nonblocking: dead suffix — m.BigM = Suffix(...) is now dead: its only entry was the removed annotation. Delete it in this PR while it is free.

Nonblocking: test strengthening — _set_purchase_count constructs an already-consistent assignment, so the equality checks are partly self-fulfilling (the closed-form comparison is the real guard). Add one negative assertion (a mismatched selector makes match_purchase_count.body nonzero) and, in the transformation test, assert the built model has zero nonlinear constraints so a regression reintroducing log fails loudly.

Coordination: PR #140 edits the same single-line "Nonlinear constraints" row of the root README size table (mod_hens columns vs stranded_gas columns here). Git cannot merge within a line — whichever lands second must regenerate the table via generate_model_size_report.py, not hand-resolve it.

Issue linkage: issue #76's checklist covers seven solver strategies, none checked. This PR removes the root cause and structurally verifies bigm/hull plus one 60 s BARON run; the strategy matrix remains unverified, so treat "Closes #76" as Refs #76 or close with an explicit note deferring the campaign.

Summary — Blocking: 0. Nonblocking: 4. Questions: 0. Tests: static verification of the tabulation math, cap, bounds, and size-report arithmetic; "12 passed" matches the test file's parametrization; CI is green at this head (6/6 checks). Merge-ready once the small cleanups and the #140 regeneration order are handled. 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 9011803 in commit 68a2873:

  • absent-type-tightening: the mtype_absent docstring now records that forcing zero purchases is a deliberate tightening and why it preserves optima (the exists branch weakly dominates since learning_factor <= 1 only lowers module unit cost under profit maximization).
  • dead-bigm-suffix: m.BigM = Suffix(...) deleted — its only entry was the annotation this PR already removed; no other reference exists in the model or tests.
  • strengthen-purchase-tests: added a negative test (a mismatched purchase-count selector makes match_purchase_count.body nonzero) and a linearity regression test asserting the built model has no nonlinear constraints, so reintroducing the log-based learning constraint fails loudly. The transformation smoke now also asserts no active Disjunction/Disjunct remains. 15 tests, up from 12.
  • xor-default-noop: body-wording correction recorded here rather than editing the description — the three xor=True edits are explicit no-ops (Pyomo's Disjunction default is already exactly-one), kept for clarity; they are not part of the behavioral fix.

Coordination reminders from the review stand: whichever of this PR and #140 lands second must regenerate the root README size table via generate_model_size_report.py (both edit the same "Nonlinear constraints" row), and issue #76's seven-strategy checklist remains unverified, so the Closes #76 link is better read as Refs #76 unless the benchmark campaign is explicitly deferred.

Tests: pixi run pytest tests/test_stranded_gas.py — 15 passed; black --check clean on both changed files. CI for the new head was starting when this was posted; the previous head was green (6/6).

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 stranded_gas to fix benchmark issues

1 participant