Skip to content

Comments

Improvements to LDSDA#3830

Merged
blnicho merged 29 commits intoPyomo:mainfrom
SECQUOIA:main
Feb 14, 2026
Merged

Improvements to LDSDA#3830
blnicho merged 29 commits intoPyomo:mainfrom
SECQUOIA:main

Conversation

@bernalde
Copy link
Contributor

@bernalde bernalde commented Feb 2, 2026

Fixes # .

Summary/Motivation:

  • A bug in ldsda.py was found related to unpacking a tuple
  • Code coverage of ldsda.py was not 100%
  • Documentation and formatting were lacking in this file
  • An extra dependency (tabulate) was unnecessarily added to the code

Changes proposed in this PR:

  • Modified line_search in pyomo/contrib/gdpopt/ldsda.py to unpack (primal_improved, primal_bound).
  • Added unit tests in pyomo/contrib/gdpopt/tests/test_ldsda.py to verify the logic handles tuple returns correctly without infinite loops and raises coverage of ldsda.py to 100%
  • Make behavior consistent with the docstring for _solve_GDP_subproblem: ensure primal_bound is None when the subproblem solve does not succeed (not only when preprocessing raises InfeasibleConstraintException).
  • Avoid adding an unnecessary global dependency: remove use of the external tabulate package from ldsda and use simple logger output instead.

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

Toflamus and others added 19 commits January 27, 2026 19:12
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Removed the documentation from the class docstring.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
capitalized the notations.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Refine test class docstring to clarify focus on critical control-flow and validation.
Refine test description for clarity and focus.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
    * the test docstring/comment no longer references specific source line numbers.
    * unused test attribute (self.number_of_external_variables) removed.

2. Changed ldsda.py:
    * update the wording to “squared Euclidean distance” in the comment describing the tiebreaker logic.
…ailure

Previously, primal_bound was computed even when the solver returned an
unsuccessful termination condition. Now primal_bound is only computed
when primal_improved is True, making the behavior consistent with the
docstring that states primal_bound is None when the subproblem is infeasible.
Replace tabulate-based reformulation summary with simple logger output,
consistent with other GDPopt solvers. This avoids adding an unnecessary
dependency to the package.
Removed the tabulate dependency and replaced it with simple logger output.
Fixed _solve_GDP_subproblem so primal_bound is None when the solver fails (not just on preprocessing infeasibility).
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@blnicho
Copy link
Member

blnicho commented Feb 3, 2026

@bernalde could you give the Pyomo team edit permissions on this PR? Should be a checkbox at the bottom of the right side information summary about "Allowing maintainers to edit this pull request".

@bernalde
Copy link
Contributor Author

bernalde commented Feb 3, 2026

Oh no, since this is coming from the SECQUOIA fork I can’t do that https://github.com/orgs/community/discussions/5634
Should I open a PR from my personal account?

Copy link
Contributor

@emma58 emma58 left a comment

Choose a reason for hiding this comment

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

Thank you for fixing the bug and adding so many tests! I put several comments in, but nothing needs to hold up the PR other than that I think you are missing assertions in one test, and there's on import that is inside of a test. The rest is all just preference and some design ideas that are totally optional.

with self.assertRaisesRegex(
ValueError, "should be a list of ExactlyExpression"
):
self.solver._get_external_information(self.model.util_block, self.config)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question about testing private methods here (and in the tests following as well.)

@blnicho
Copy link
Member

blnicho commented Feb 3, 2026

Oh no, since this is coming from the SECQUOIA fork I can’t do that https://github.com/orgs/community/discussions/5634 Should I open a PR from my personal account?

Ok, no problem. I didn't know about that limitation. We'll work with this PR, no need to reopen it from a personal fork.

…s, make get_external_information public

- Add DirectionNorm enum (L2, Linf) with backward compatibility for string values
- Add SearchPhase enum (INITIAL, NEIGHBOR, LINE) for search type
- Rename _get_external_information to get_external_information (public API)
- Update all usages and tests accordingly
@blnicho blnicho moved this from Todo to Review In Progress in Pyomo 6.10 Feb 4, 2026
@codecov
Copy link

codecov bot commented Feb 4, 2026

Codecov Report

❌ Patch coverage is 96.29630% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 89.75%. Comparing base (89ebf89) to head (1f63298).
⚠️ Report is 33 commits behind head on main.

Files with missing lines Patch % Lines
pyomo/contrib/gdpopt/ldsda.py 96.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3830      +/-   ##
==========================================
+ Coverage   89.73%   89.75%   +0.01%     
==========================================
  Files         904      904              
  Lines      105870   105885      +15     
==========================================
+ Hits        95005    95035      +30     
+ Misses      10865    10850      -15     
Flag Coverage Δ
builders 29.04% <40.74%> (+0.01%) ⬆️
default 83.87% <96.29%> (?)
expensive 35.50% <40.74%> (?)
linux 86.92% <66.66%> (-2.56%) ⬇️
linux_other 86.92% <66.66%> (+0.08%) ⬆️
oldsolvers 29.87% <40.74%> (-0.01%) ⬇️
osx 83.09% <66.66%> (+0.08%) ⬆️
win 85.15% <66.66%> (+0.08%) ⬆️
win_other 85.15% <66.66%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@emma58 emma58 left a comment

Choose a reason for hiding this comment

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

A couple comments on the change to enums, but otherwise this looks good to me!

The dimensionality of the neighborhood (number of external variables).
config : ConfigBlock
GDPopt configuration block
The configuration block specifying the norm ('L2' or 'Linf').
Copy link
Contributor

Choose a reason for hiding this comment

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

You probably want to update this docstring to reference your new DirectionNorm enum.

Copy link
Contributor

Choose a reason for hiding this comment

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

My apologies for the late response. It is a good point. Since DirectionNorm is a str-Enum, the legacy == 'L2'/'Linf' checks would work with either enum or string inputs. The explicit tuple membership check was retained mainly to make it visually clear that both DirectionNorm values and legacy string inputs are accepted. I am happy to simplify it if you'd prefer.

Copy link
Contributor

Choose a reason for hiding this comment

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

I personally think simplifying it is better, and that you should encourage users to use the enum rather than relying on string arguments. We get burned relying on strings a lot and have been trying to move away from it. But I'm not opposed to this going in as is if that's your preference.

Copy link
Contributor

Choose a reason for hiding this comment

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

(Though I do think you may as well make the reversions in the comments below to just check the equality like before.)

Comment on lines 329 to 421
if config.direction_norm == 'L2':
if config.direction_norm in (DirectionNorm.L2, 'L2'):
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you set the value of the enum to be the string, the old version should work in either case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since DirectionNorm is a str-Enum, another clean option would be to normalize once using norm = str(config.direction_norm) and then compare against the string values. This keeps compatibility with legacy string inputs while avoiding redundant membership checks.
Something like,

norm = str(config.direction_norm)
if norm == 'L2':
    ...
elif norm == 'Linf':
    ...

Copy link
Contributor

@emma58 emma58 Feb 13, 2026

Choose a reason for hiding this comment

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

You don't need to cast it to a string--it's already a string. You can just make this line:

if config.direction_norm == DirectionNorm.L2

(This works because DirectionNorm.L2 == 'L2')

Copy link
Contributor

Choose a reason for hiding this comment

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

(Sorry, just realized my original comment was confusing. What I meant is above.)

Comment on lines 335 to 427
elif config.direction_norm == 'Linf':
elif config.direction_norm in (DirectionNorm.Linf, 'Linf'):
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here: Since you set the value of the enum to be the string, the old version should work in either case.

Copy link
Contributor

Choose a reason for hiding this comment

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

If normalizing the DirectionNorm using norm = str(config.direction_norm) is a good idea, I will implement it.

@github-project-automation github-project-automation bot moved this from Review In Progress to Reviewer Approved in Pyomo 6.10 Feb 10, 2026
@blnicho blnicho moved this from Reviewer Approved to Review In Progress in Pyomo 6.10 Feb 11, 2026
@blnicho
Copy link
Member

blnicho commented Feb 12, 2026

@bernalde or @AlbertLee125 could you reply to Emma's review comments or mark them resolved if there isn't anything to be done?

@AlbertLee125
Copy link
Contributor

My apologies for the late response. It is a good point. Since DirectionNorm is a str-Enum, the legacy == 'L2'/'Linf' checks would work with either enum or string inputs. The explicit tuple membership check was retained mainly to make it visually clear that both DirectionNorm values and legacy string inputs are accepted. I am happy to simplify it if you'd prefer.

@AlbertLee125
Copy link
Contributor

I applied the suggested change. Backward compatibility remains intact. Thank you very much for your kind instruction.

Comment on lines 11 to 21
from pyomo.environ import (
SolverFactory,
value,
Var,
Constraint,
TransformationFactory,
ConcreteModel,
BooleanVar,
LogicalConstraint,
Block,
)
Copy link
Member

Choose a reason for hiding this comment

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

@AlbertLee125 this looks like a bad merge. You lost some of the imports required for this test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for pointing that out. It was a merge issue on my side. I restored the missing imports.

@github-project-automation github-project-automation bot moved this from Review In Progress to Reviewer Approved in Pyomo 6.10 Feb 14, 2026
@blnicho
Copy link
Member

blnicho commented Feb 14, 2026

Jenkins failure is unrelated (it's an intermittent cuOpt failure). This is safe to merge.

@blnicho blnicho merged commit 634a186 into Pyomo:main Feb 14, 2026
34 of 35 checks passed
@github-project-automation github-project-automation bot moved this from Reviewer Approved to Done in Pyomo 6.10 Feb 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

6 participants