Repository: Pyomo/pyomo

Issue:
PETSc DAE Solver Variables not in nl-File (IDAES)
I need to do something unusual with the nl writer for sending problems to the AMPL wrapper for the PETSc DAE solver.  Sometimes in a DAE problem there are differential variables that don't appear in any equation.  Usually there would be no reason for these variables to appear in the nl-file, but in the case of the DAE solver we do solve for these variables and want them to appear in the results.  My current work around is to just add some dummy constraints that are simple but include the missing differential variables.  What I would like to do is force the NL writer to include some variables even though they don't appear in the constraints.  This would result in a Jacobian that would have empty columns, and I'm not sure if the ASL will complain.  This could possibly be easy to address by adding an argument to the NL writer that takes a set of variables to force into the NL writer.  I guess it remains to be seen how the ASL would take it. 


Discussion:
This is also related to the following discussion: 

> Do variables exist to a solver if they are not attached to a constraint?
For the PETSc DAE solver, I did a quick proof-of-concept and forcing differential variables into the NL file does work.  I'm not sure what the best way to do this is but I'll submit a simple PR, and we can go from there.

Resolving patch:
diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py
--- a/pyomo/repn/plugins/ampl/ampl_.py
+++ b/pyomo/repn/plugins/ampl/ampl_.py
@@ -351,6 +351,11 @@ def __call__(self,
         include_all_variable_bounds = \
             io_options.pop("include_all_variable_bounds", False)
 
+        # List of variables that don't appear in constraints to force into the
+        # nl-file
+        export_nonlinear_variables = \
+            io_options.pop("export_nonlinear_variables", False)
+
         if len(io_options):
             raise ValueError(
                 "ProblemWriter_nl passed unrecognized io_options:\n\t" +
@@ -399,7 +404,8 @@ def __call__(self,
                     show_section_timing=show_section_timing,
                     skip_trivial_constraints=skip_trivial_constraints,
                     file_determinism=file_determinism,
-                    include_all_variable_bounds=include_all_variable_bounds)
+                    include_all_variable_bounds=include_all_variable_bounds,
+                    export_nonlinear_variables=export_nonlinear_variables)
 
         self._symbolic_solver_labels = False
         self._output_fixed_variable_bounds = False
@@ -700,7 +706,8 @@ def _print_model_NL(self, model,
                         show_section_timing=False,
                         skip_trivial_constraints=False,
                         file_determinism=1,
-                        include_all_variable_bounds=False):
+                        include_all_variable_bounds=False,
+                        export_nonlinear_variables=False):
 
         output_fixed_variable_bounds = self._output_fixed_variable_bounds
         symbolic_solver_labels = self._symbolic_solver_labels
@@ -1136,6 +1143,15 @@ def _print_model_NL(self, model,
             UnusedVars = AllVars.difference(UsedVars)
             LinearVars.update(UnusedVars)
 
+        if export_nonlinear_variables:
+            for v in export_nonlinear_variables:
+                v_iter = v.values() if v.is_indexed() else iter((v,))
+                for vi in v_iter:
+                    if self_varID_map[id(vi)] not in UsedVars:
+                        Vars_dict[id(vi)] = vi
+                        ConNonlinearVars.update([self_varID_map[id(vi)]])
+
+
         ### There used to be an if statement here for the following code block
         ### checking model.statistics.num_binary_vars was greater than zero.
         ### To this day, I don't know how it worked.


Test patch:
diff --git a/pyomo/repn/tests/ampl/test_ampl_nl.py b/pyomo/repn/tests/ampl/test_ampl_nl.py
--- a/pyomo/repn/tests/ampl/test_ampl_nl.py
+++ b/pyomo/repn/tests/ampl/test_ampl_nl.py
@@ -43,6 +43,81 @@ def _get_fnames(self):
         prefix = os.path.join(thisdir, test_name.replace("test_", "", 1))
         return prefix+".nl.baseline", prefix+".nl.out"
 
+    def test_export_nonlinear_variables(self):
+        model = ConcreteModel()
+        model.x = Var()
+        model.y = Var()
+        model.z = Var()
+        model.w = Var([1,2,3])
+        model.c = Constraint(expr=model.x == model.y**2)
+
+        model.y.fix(3)
+        test_fname = "export_nonlinear_variables"
+        model.write(
+            test_fname,
+            format='nl',
+            io_options={'symbolic_solver_labels':True}
+        )
+        with open(test_fname + '.col') as f:
+            names = list(map(str.strip, f.readlines()))
+        assert "z" not in names # z is not in a constraint
+        assert "y" not in names # y is fixed
+        assert "x" in names
+        self._cleanup(test_fname)
+        model.write(
+            test_fname,
+            format='nl',
+            io_options={
+                'symbolic_solver_labels':True,
+                'export_nonlinear_variables':[model.z]
+            }
+        )
+        with open(test_fname + '.col') as f:
+            names = list(map(str.strip, f.readlines()))
+        assert "z" in names
+        assert "y" not in names
+        assert "x" in names
+        assert "w[1]" not in names
+        assert "w[2]" not in names
+        assert "w[3]" not in names
+        self._cleanup(test_fname)
+        model.write(
+            test_fname,
+            format='nl',
+            io_options={
+                'symbolic_solver_labels':True,
+                'export_nonlinear_variables':[model.z, model.w]
+            }
+        )
+        with open(test_fname + '.col') as f:
+            names = list(map(str.strip, f.readlines()))
+        assert "z" in names
+        assert "y" not in names
+        assert "x" in names
+        assert "w[1]" in names
+        assert "w[2]" in names
+        assert "w[3]" in names
+
+        self._cleanup(test_fname)
+
+        model.write(
+            test_fname,
+            format='nl',
+            io_options={
+                'symbolic_solver_labels':True,
+                'export_nonlinear_variables':[model.z, model.w[2]]
+            }
+        )
+        with open(test_fname + '.col') as f:
+            names = list(map(str.strip, f.readlines()))
+        assert "z" in names
+        assert "y" not in names
+        assert "x" in names
+        assert "w[1]" not in names
+        assert "w[2]" in names
+        assert "w[3]" not in names
+
+        self._cleanup(test_fname)
 
     def test_var_on_other_model(self):
         other = ConcreteModel()