Skip to content

Conversation

michaelbynum
Copy link
Contributor

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.

Copy link

codecov bot commented Aug 14, 2025

Codecov Report

❌ Patch coverage is 88.79813% with 192 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.94%. Comparing base (2ca7dfb) to head (ecd602d).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
...contrib/solver/solvers/gurobi/gurobi_persistent.py 89.25% 86 Missing ⚠️
pyomo/contrib/observer/model_observer.py 90.08% 49 Missing ⚠️
pyomo/contrib/solver/common/solution_loader.py 72.30% 18 Missing ⚠️
...ontrib/solver/solvers/gurobi/gurobi_direct_base.py 89.58% 15 Missing ⚠️
...omo/contrib/solver/solvers/gurobi/gurobi_direct.py 93.54% 8 Missing ⚠️
pyomo/contrib/solver/solvers/sol_reader.py 63.15% 7 Missing ⚠️
pyomo/contrib/solver/solvers/highs.py 70.58% 5 Missing ⚠️
pyomo/contrib/observer/component_collector.py 94.28% 2 Missing ⚠️
pyomo/contrib/solver/solvers/ipopt.py 77.77% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main    #3703    +/-   ##
========================================
  Coverage   85.94%   85.94%            
========================================
  Files         892      895     +3     
  Lines      103018   103738   +720     
========================================
+ Hits        88535    89158   +623     
- Misses      14483    14580    +97     
Flag Coverage Δ
builders 26.76% <23.80%> (+0.02%) ⬆️
default 85.77% <88.79%> (?)
expensive 34.04% <23.92%> (?)
linux ?

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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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?

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.

3 participants