Skip to content

[Depends on #3701] Add tests for trivial constraints in pyomo/contrib/solver#3703

Open
michaelbynum wants to merge 54 commits intoPyomo:mainfrom
michaelbynum:trivial_constraints
Open

[Depends on #3701] Add tests for trivial constraints in pyomo/contrib/solver#3703
michaelbynum wants to merge 54 commits intoPyomo:mainfrom
michaelbynum:trivial_constraints

Conversation

@michaelbynum
Copy link
Copy Markdown
Contributor

@michaelbynum michaelbynum commented Aug 14, 2025

Summary/Motivation:

This PR adds a test for trivial constraints (both feasible and infeasible) to the new solver tests. It also fixes some related bugs.

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.

@codecov
Copy link
Copy Markdown

codecov bot commented Aug 14, 2025

Codecov Report

❌ Patch coverage is 84.37500% with 35 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.68%. Comparing base (6ab0106) to head (75fee85).
⚠️ Report is 19 commits behind head on main.

Files with missing lines Patch % Lines
pyomo/contrib/solver/common/solution_loader.py 78.46% 14 Missing ⚠️
...ontrib/solver/solvers/gurobi/gurobi_direct_base.py 65.78% 13 Missing ⚠️
pyomo/contrib/solver/solvers/highs.py 85.71% 3 Missing ⚠️
pyomo/contrib/solver/solvers/asl_sol_reader.py 95.55% 2 Missing ⚠️
pyomo/contrib/solver/solvers/gms_sol_reader.py 94.73% 1 Missing ⚠️
pyomo/contrib/solver/solvers/ipopt.py 80.00% 1 Missing ⚠️
pyomo/contrib/solver/solvers/knitro/solution.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3703      +/-   ##
==========================================
- Coverage   89.92%   88.68%   -1.25%     
==========================================
  Files         902      902              
  Lines      106393   106557     +164     
==========================================
- Hits        95679    94505    -1174     
- Misses      10714    12052    +1338     
Flag Coverage Δ
builders 29.19% <32.58%> (+<0.01%) ⬆️
default 86.23% <84.37%> (?)
expensive 35.63% <32.58%> (?)
linux 61.73% <55.35%> (-27.69%) ⬇️
linux_other 61.73% <55.35%> (-25.66%) ⬇️
oldsolvers 28.10% <32.58%> (-0.01%) ⬇️
osx ?
win ?
win_other ?

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.

return self.var_map[self.v2id]


class GurobiDirectQuadratic(GurobiDirectBase):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't this class be named GurobiConsistentQuadratic? In GurobiObserver this is the used name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm confused. Can you point me to to the place in GurobiObserver you are referring to?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I was referring to the field opt of the class _GurobiObserver.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This class does not use the observer. This one is not persistent. Maybe I am missing something?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, maybe I wasn’t clear enough. From what I see, in _GurobiObserver you are requesting opt as a GurobiPersistantQuadratic, but this class doesn’t appear to be defined. However, GurobiPersistant inherits from GurobiDirectQuadratic and is what gets passed to _GurobiObserver. So either I’m missing something, or there’s a naming issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Thank you. The observer is expecting GurobiPersistent not GurobiPersistentQuadratic. I will fix that.

self._solver_model.setObjective(gurobi_expr + repn_constant, sense=sense)


class _GurobiObserver(Observer):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this class is just calling the "same" functions on the opt field. Why not just inherits Observer in GurobiPersistantSolver?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It has to be done this way in order for "manual mode" of the persistent solvers to work. If someone calls opt.add_constraint directly on the solver interface, we need the observer to be updated so things stay synchronized.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You’re probably right, and I might be missing something. However, I don’t see why adding Observer as a superclass of GurobiPersistent wouldn't mean the observer is always updated, since it’s essentially the same as the persistent object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The important thing is that the _change_detector methods get called. This makes my head hurt a little. Let me think on it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So if we did something like

class GurobiPersistent(GurobiDirectQuadratic, Observer):
    def add_constraints(self, cons):
        self._change_detector.add_constraints(cons)

Then there would not be a way to call _add_constraints. It would just be an infinte loop. If GurobiPersistent.add_constraints gets called, it would call the _change_detector, and the _change_detector would again call add_constraints on the observer, which is GurobiPersistent.add_constraints.

That is convoluted, but did it make any sense?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With the current design, this will always happen: the solver calls the detector, which calls the observer, which in turn calls the solver (Opt) again. Or am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If GurobiPersistent.add_constraints gets called, then that will call ModelChangeDetector.add_constraints, which will then call _GurobiObserver.add_constraints, which then calls GurobiPersistent._add_constraints and not GurobiPersistent.add_constraints. Very subtle but important difference there. However, this conversation is making me realize how convoluted this is. Maybe someone has a better idea?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Okay, understood! One possible fix would be to give all Observer functions a common prefix (e.g., on_*) and make GurobiPersistantSolver a subclass of Observer. We could then reimplement the functions accordingly.

raise NoSolutionError()

def load_vars(
self, vars_to_load: Sequence[VarData] | None = None, solution_id=None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't we use Optional here?

@blnicho blnicho removed this from Pyomo 6.10 Feb 4, 2026
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.

5 participants