Skip to content

Conversation

michaelbynum
Copy link
Contributor

Summary/Motivation:

This PR extracts the code for detecting changes in models from the persistent solver interfaces to its own, independent functionality. I will have a separate PR shortly that removes the code from the solver interfaces and updates them to use this code.

Changes proposed in this PR:

  • add a module to contrib called observer that can be used to inform other classes of what changed in a pyomo model.

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 13, 2025

Codecov Report

❌ Patch coverage is 91.39073% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.33%. Comparing base (7816915) to head (bb60959).

Files with missing lines Patch % Lines
pyomo/contrib/observer/model_observer.py 90.79% 50 Missing ⚠️
pyomo/contrib/observer/component_collector.py 96.72% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3695      +/-   ##
==========================================
+ Coverage   89.27%   89.33%   +0.05%     
==========================================
  Files         896      898       +2     
  Lines      103687   104291     +604     
==========================================
+ Hits        92567    93166     +599     
- Misses      11120    11125       +5     
Flag Coverage Δ
builders 29.06% <21.68%> (?)
default 85.96% <91.39%> (?)
expensive 35.78% <21.68%> (?)
linux 86.99% <91.36%> (-2.06%) ⬇️
linux_other 86.99% <91.36%> (+0.02%) ⬆️
osx 83.14% <91.36%> (+0.04%) ⬆️
win 85.26% <91.36%> (+0.04%) ⬆️
win_other 85.26% <91.36%> (+0.04%) ⬆️

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.

@mrmundt mrmundt self-requested a review August 19, 2025 18:39
Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

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

Overall, this is great. I have lots of questions, but very few should prohibit merging this.



def handle_var(node, collector):
collector.variables[id(node)] = node
Copy link
Member

Choose a reason for hiding this comment

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

This is basically reimplementing a version of ComponentSet. Is there a reason not to re-use that 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.

I have found that doing this manually is faster. That is the only reason. I'm not sure if the difference is enough to justify or not.

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'll try to get some numbers.

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 seems to be about twice as fast.

Copy link
Member

Choose a reason for hiding this comment

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

OK - but we ought to see why ComponentSet is slower and try to fix it there.

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 don't disagree, but I don't think that needs to be part of the PR. We can always change the underlying implementation of this later. None of this is exposed to the user.

Comment on lines 617 to 618
elif v.value != _value:
vars_to_update.append(v)
Copy link
Member

Choose a reason for hiding this comment

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

Don't you also have to update a constraint if the var is fixed?

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 think this is up to the observer... Worth a discussion.

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 not sure what the best solution is for this. I think we need more use cases. I vote we leave it for now. Here are a couple of examples that make this challenging:

m.x * (m.y + m.z) == 0
m.x.fix(1)
opt.solve(m)
m.x.value = 2

Right now, this works fine for the existing persistent solver interfaces because changing the value of m.x does not change the "structure" of the constraint (i.e., it remains linear). Essentially, we create a "mutable linear coefficient" and update that coefficient when the value of the variable changes. Furthermore, this does not require reconstructing the constraint in any way. However,

(m.y + m.z) ** m.x == 0
m.x.fix(1)

will currently just result in an exception for both gurobi persistent and highs persistent (scip is fine).

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've been thinking about this more. Maybe we can add some public methods to ModelChangeDetector to provide more information if necessary. For example, we could add a method to get all of the constraints used by a variable (the change detector already has that data).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jsiirola and I discussed this further and agreed that expanding the interface was a good solution. I added to the docstring explaining that we can expand the interface, but we are waiting to do so until we have a need.

@jsiirola
Copy link
Member

Additional question: who is responsible for verifying that the model does not contain any active components that the Solver/Observer doesn't know how to handle? This appears to assume that the responsibility lies with the Solver, but does that mean that the solver needs to also implement it's own observer to make sure that "unallowable" active components aren't added between solves? That makes me think that maybe the responsibility needs to lie in this observer...

@michaelbynum
Copy link
Contributor Author

I think I have addressed everything, @jsiirola and @mrmundt. Should be ready to go.

@mrmundt mrmundt self-requested a review September 17, 2025 20:34
Copy link
Contributor

@mrmundt mrmundt left a comment

Choose a reason for hiding this comment

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

This looks good! Thanks for doing all the fixes.

)
)
)
self._add_sos_constraints(
Copy link
Contributor

Choose a reason for hiding this comment

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

Some solvers do not support sos constraint. How about making this optional?

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 think that the derived classes can choose to raise NotImplementedError. I'd rather they do so consciously.

@eminyouskn
Copy link
Contributor

LGTM. One minor comment but not that important.

@michaelbynum
Copy link
Contributor Author

Okay, if tests pass, I think we can merge this.

Copy link
Member

@jsiirola jsiirola left a comment

Choose a reason for hiding this comment

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

I think this is OK, but there are a few things I keep coming back to.

Some minor comments that would be nice to get in (and quick to address), along with some longer discussions that can (probably) be deferred to subsequent PRs.

I also unresolved a number of discussions so we can find them more readily when we are working on subsequent PRs.



def handle_var(node, collector):
collector.variables[id(node)] = node
Copy link
Member

Choose a reason for hiding this comment

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

OK - but we ought to see why ComponentSet is slower and try to fix it there.

def exitNode(self, node, data):
return collector_handlers[node.__class__](node, self)

def beforeChild(self, node, child, child_idx):
Copy link
Member

Choose a reason for hiding this comment

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

(I mentioned this in a previous comment, so repeating here so it is not lost for future development / reference)

For performance reasons, it may be good to add a BeforeChildDispatcher as well (especially because this collector is specifically looking for leaf nodes).

That said, this improvement can be deferred to a subsequent PR.

Copy link
Contributor Author

@michaelbynum michaelbynum Oct 6, 2025

Choose a reason for hiding this comment

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

I would prefer to do this as a separate PR.

import gc


"""
Copy link
Member

Choose a reason for hiding this comment

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

If you either move this before the imports, or make the following change,this string will be picked up by the RTD API docs:

Suggested change
"""
__doc__ = """

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 did not intend this to be user documentation. The docstring for ModelChangeDetector is mean to be the documentation.

Comment on lines +578 to +579
ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id]
if not ref_cons and not ref_sos and not ref_obj:
Copy link
Member

Choose a reason for hiding this comment

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

I realize that I actually suggested the current syntax, but thinking more on it, it might nice to consider (in the future) the following:

            if not any(self._referenced_variables[v_id]):

(same functionality, possible clearer, and avoids the creation / access / destruction of unnecessary entries in locals(); unknown if saving the intermediate variables is faster / slowed than the call to any())

This idiom can be used here and numerous places below.

Comment on lines +43 to +44
- modified constraint expressions (relies on expressions being immutable)
- modified objective expressions (relies on expressions being immutable)
Copy link
Member

Choose a reason for hiding this comment

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

It is still weird to me that we make the client remember the mapping from Variable / Param values if they need to manually update constraints (e.g., due to fixed variables / Params changing values). This is implicit in this documentation, too: we will update the user on structural changes to the expressions, but not numeric changes ... and if a numeric change causes a structural change, that is (hopefully) caught by the client (if noly they know they need to look for it)?

Can we at least make these mappings public so that the client can reuse them?

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 thought we discussed this? We can expand the API as needed to support this.

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 don't think we can make the mappings public because they should not be modified. Instead we can add methods for clients to request the information they need. I don't think that needs done as part of this PR.

@michaelbynum
Copy link
Contributor Author

Okay, should be good to go, @jsiirola (assuming tests pass).

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

5 participants