Repository: Pyomo/pyomo

Issue:
NL Writer Errors when presolve removes all variables
## Summary

It would be extremely convenient if we could just return the solution somehow.

```
>>> import pyomo.environ as pe
>>> from pyomo.repn.plugins.nl_writer import NLWriter
>>> import io
>>> m = pe.ConcreteModel()
>>> m.x = pe.Var()
>>> m.y = pe.Var()
>>> m.obj = pe.Objective(expr=m.y)
>>> m.c1 = pe.Constraint(expr=m.y == m.x + 2)
>>> m.c2 = pe.Constraint(expr=m.y == -m.x + 1)
>>> writer = NLWriter()
>>> out = io.StringIO()
>>> writer.write(m, out)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../pyomo/repn/plugins/nl_writer.py", line 397, in write
    return impl.write(model)
           ^^^^^^^^^^^^^^^^^
  File "/.../pyomo/repn/plugins/nl_writer.py", line 865, in write
    raise ValueError(
ValueError: No variables appear in the Pyomo model constraints or objective. This is not supported by the NL file interface
```



Discussion:
I left that in the new writer because it existed in the original NL writer.  I agree that with the new presolve it is more likely that legitimate models will be sent to a solver and solved in the presolve.  How do you think we should communicate this between the writer and the solver?  My recollection when I was working on the NL writer was that ASL solvers would die a pretty terrible death if they were handed an NL file with no variables, so we would need to move this check from the NL writer to the solver interface(s).  I suppose the information is in the NLInfo object, but we would need to make sure that all the NL solver interfaces handled it correctly.
Excellent question. I actively avoided attempting to figure that out in the original issue, haha. Maybe the NL writer could have an option to skip the error and return the NLWriterInfo anyway. Then the caller can see if `info.variables` is empty and decide what to do?
I think that sounds quite reasonable.  What do you think about preserving the old behavior in the legacy "call" interface (so the old solvers don't have to be updated).  That is `__call__` will raise the exception (and delete the NL file), whereas the `write()` interface will just write and return ... and leave that particular error check up to the solver interface?
Sounds good to me!

Resolving patch:
diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py
--- a/pyomo/repn/plugins/nl_writer.py
+++ b/pyomo/repn/plugins/nl_writer.py
@@ -346,6 +346,18 @@ def __call__(self, model, filename, solver_capability, io_options):
             row_fname
         ) as ROWFILE, _open(col_fname) as COLFILE:
             info = self.write(model, FILE, ROWFILE, COLFILE, config=config)
+        if not info.variables:
+            # This exception is included for compatibility with the
+            # original NL writer v1.
+            os.remove(filename)
+            if config.symbolic_solver_labels:
+                os.remove(row_fname)
+                os.remove(col_fname)
+            raise ValueError(
+                "No variables appear in the Pyomo model constraints or"
+                " objective. This is not supported by the NL file interface"
+            )
+
         # Historically, the NL writer communicated the external function
         # libraries back to the ASL interface through the PYOMO_AMPLFUNC
         # environment variable.
@@ -854,13 +866,6 @@ def write(self, model):
         con_vars = con_vars_linear | con_vars_nonlinear
         all_vars = con_vars | obj_vars
         n_vars = len(all_vars)
-        if n_vars < 1:
-            # TODO: Remove this.  This exception is included for
-            # compatibility with the original NL writer v1.
-            raise ValueError(
-                "No variables appear in the Pyomo model constraints or"
-                " objective. This is not supported by the NL file interface"
-            )
 
         continuous_vars = set()
         binary_vars = set()


Test patch: