Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NL writer: Add custom exception for no free variables #3445

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

alma-walmsley
Copy link
Contributor

Fixes #3411.

Summary/Motivation:

nl_writer.py raises a ValueError for when no free variables are found when writing the nl file. However, this is potentially a valid case in my situation (ie. everything is defined; all expressions can just be evaluated). I don't want to try...catch for a ValueError because it is a pretty generic exception, and I do want to error if there are other problems.

See the linked issue for a full description.

Changes proposed in this PR:

  • Add a custom exception NLWriterEmptyModelError, which inherits from ValueError. This means a try...catch around the solve statement (with this specific exception) can be used to handle this case gracefully.

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.

@alma-walmsley
Copy link
Contributor Author

alma-walmsley commented Dec 8, 2024

Related to #3131 / #3135. However, that indicates that this error should only occur when the legacy __call__ method is used.

For context, I am running into this issue with:

solver = SolverFactory("ipopt")
solver.solve(m)

@alma-walmsley
Copy link
Contributor Author

Some feedback may be useful as I'm not sure if there are any implications on presolved values or other things I'm unaware of. My use case is "everything is defined", so solving can be skipped.

Comment on lines 88 to 94
class NLWriterEmptyModelError(ValueError):
"""
A custom exception to allow handling of a
model with no free variables.
"""

pass
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion here: We have a "custom" exception from pyomo/common/errors called PyomoException that you might want to inherit from instead. We do this fairly frequently for custom error types so it is also clear when an exception comes from Pyomo as opposed to something else (e.g., gurobipy or idaes).

Example:

from pyomo.common.errors import PyomoException

class MyCustomError(PyomoException):
   """
   My custom error
   """

Also note that you don't need to put pass as long as you have a docstring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, I initially had it inheriting from ValueError since that was raised previously, but I don't suppose there is much need for backwards compatibility in this case!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nevermind, there were a couple of tests designed to catch this - I have replaced the asserted exception in those tests

@alma-walmsley
Copy link
Contributor Author

pyomo/repn/plugins/ampl/ampl_.py has the same exception in the _print_model_NL function (line 1365) ... should I look at adding the custom exception there as well?

@jsiirola
Copy link
Member

I have spent the last week debating (with myself [and losing]) what the "expected" behavior here should be.

The current implementation dates back to the original NL writer, and followed the logic that since some mix of AMPL / the solver will die a horrible death of you give it an NL file with no variables, then we should consider a model with no free variables an "error" (and raise an exception). That seemed reasonable, but now with the introduction of the presolver, it is possible that a "valid" model could be square and completely solvable in the presolve (leaving us with an empty NL file). In this case, we can't raise an exception because we need the NL writer to return the presolve information so that we (the solver / caller) can record / load the computed values into the variables. That means that we need to move the check for an "empty" NL file from the writer and into the solver, and raises the question as to if this exception should be removed entirely. The argument against that is that an "empty" model can be a sign of a modeling error (something like forgetting to correctly load data into indexing sets, so everything is empty), and the exception was meant to prevent the user from assuming that everything was fine (when in fact there was nothing to do).

I am wondering if the following is a reasonable path forward:

  • this PR is a good (and necessary) change (we have long held that Pyomo needs to revisit the exceptions it raises and add specificity where it makes sense). That said:
    • I think this is a more general issue than just the NL writer, so I would propose renaming it EmptyModelError
    • it should be defined in pyomo/common/errors.py
    • it should inherit from both ValueError (for backwards compatibility) and PyomoException (for consistency with other Pyomo errors)
    • we should use this error in the NL writers (v1 and v2), and probably the LP, BAR, and GAMS writers as well
  • we should deprecate the behavior of raising EmptyModelError exceptions in the writers. Since there is no way to change the behavior (to not raise an exception) and to preserve backwards compatibility, I propose logging a deprecation warning before raising the error and then adding functionality to pyomo/future.py to allow users to switch to the "new" behavior (where no error is raised).
    • it would probably be good for this to be both a global switch (for changing the behavior globally) and a context manager (so specific solver interfaces can move to the new model early).
    • it is an open question (at least for me) as to if the solvers should now raise the EmptyModelError exception for "truly empty" models, or if they should just log a warning.

Thoughts?

@alma-walmsley
Copy link
Contributor Author

This sounds good to me. To summarise:

  • Keep the empty model check for the old nl writer interface(s), but it should use an EmptyModelError
  • The new nl writer interface leaves the check up to the solver interface that called it.
    • Don't raise an exception for empty models because of a successful presolve - maybe include a log message? In any case, the presolved values need to be loaded back into the model.
    • Either raise the EmptyModelException or log a warning for "truly empty" models.

@alma-walmsley
Copy link
Contributor Author

alma-walmsley commented Dec 18, 2024

If I understand correctly, the new solver interface already has support for skipping a solve if it gets presolved?

Something like this works:

from pyomo.environ import ConcreteModel, Var, Constraint
from pyomo.contrib.solver.factory import SolverFactory

m = ConcreteModel()
m.x = Var()
m.c = Constraint(expr=m.x == 1)

opt = SolverFactory("ipopt")
result = opt.solve(m, tee=True)

print(m.x.value)

but interestingly, this doesn't (is this any different from the "truly empty" case?)

from pyomo.environ import ConcreteModel, Var
from pyomo.contrib.solver.factory import SolverFactory

m = ConcreteModel()
m.x = Var()
m.x.fix(1)

opt = SolverFactory("ipopt")
result = opt.solve(m, tee=True)

print(m.x.value)

@alma-walmsley
Copy link
Contributor Author

Also, I think it doesn't necessarily make sense to limit the scope of this PR to just fixing the exception raised by the old nl writer. It is probably a good idea to fix things properly (and on my end, we should look at moving to the new solver factory interface).

@mrmundt
Copy link
Contributor

mrmundt commented Dec 18, 2024

@alma-walmsley - preemptive warning that I have a PR that is going to be up sometime in the next few weeks that restructures the new solver interfaces a bit, so the import path won't be exactly the same (it'll most likely be something like pyomo.contrib.solver.common.factory).

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.

NL Writer: no variables appear
3 participants