Repository: Pyomo/pyomo

Issue:
Limitation with arguments to ExternalFunctions
I have a case where I am using `ExternalFunctions` as part of my model. The `ExternalFunction` takes three arguments:

* a (constant) integer argument indicating an internal option for the function (in this case different forms of cubic equations of state),
* 2 arguments (Variables) from the model.

As the first argument is a constant, internal option I would like to provide it as a pure Python variable rather than a Pyomo component. However, if I provide the first argument as a Python `int` then I get the following error:

```
Traceback (most recent call last):
  File "dummy_frame.py", line 44, in <module>
    results = solver.solve(m, tee=True)
  File "/home/alee/pyomo/pyomo/pyomo/opt/base/solvers.py", line 573, in solve
    self._presolve(*args, **kwds)
  File "/home/alee/pyomo/pyomo/pyomo/opt/solver/shellcmd.py", line 198, in _presolve
    OptSolver._presolve(self, *args, **kwds)
  File "/home/alee/pyomo/pyomo/pyomo/opt/base/solvers.py", line 673, in _presolve
    **kwds)
  File "/home/alee/pyomo/pyomo/pyomo/opt/base/solvers.py", line 744, in _convert_problem
    **kwds)
  File "/home/alee/pyomo/pyomo/pyomo/opt/base/convert.py", line 105, in convert_problem
    problem_files, symbol_map = converter.apply(*tmp, **tmpkw)
  File "/home/alee/pyomo/pyomo/pyomo/solvers/plugins/converter/model.py", line 191, in apply
    io_options=io_options)
  File "/home/alee/pyomo/pyomo/pyomo/core/base/block.py", line 1716, in write
    io_options)
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 378, in __call__
    include_all_variable_bounds=include_all_variable_bounds)
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 1531, in _print_model_NL
    wrapped_repn.repn.nonlinear_expr)
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 529, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(exp.arg(1))
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 528, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(exp.arg(0))
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 503, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(vargs[0])
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 529, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(exp.arg(1))
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 589, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(exp.arg(0))
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 528, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(exp.arg(0))
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 503, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(vargs[0])
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 529, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(exp.arg(1))
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 635, in _print_nonlinear_terms_NL
    self._print_nonlinear_terms_NL(exp.expr)
  File "/home/alee/pyomo/pyomo/pyomo/repn/plugins/ampl/ampl_.py", line 569, in _print_nonlinear_terms_NL
    elif arg.is_fixed():
AttributeError: 'int' object has no attribute 'is_fixed'
```

The reason is that line 569 in ampl_.py presumes that the argument is a Pyomo component, and thus has the `is_fixed` attribute:

```
                for arg in exp.args:
                    print(arg)
                    if isinstance(arg, basestring):
                        OUTPUT.write(string_arg_str % (len(arg), arg))
                    elif arg.is_fixed():
                        self._print_nonlinear_terms_NL(arg())
                    else:
                        self._print_nonlinear_terms_NL(arg)
```

Obviously, this precludes using anything other than a `Param` or `Var` for these arguments. However, I would prefer to avoid creating a Pyomo component just for this purpose. Would it be possible to add support for non-Pyomo components are arguments to `ExternalFunctions`?


Discussion:
I would like to echo this sentiment. It does not appear to be isolated to just this case. Just running the simple code below triggers the same error for me:

```
    m = ConcreteModel()
    m.x = Var(bounds=(1, 2))
    def myfun(x):
        return x
    m.fun = ExternalFunction(function=myfun)
    m.obj = Objective(expr=m.fun(m.x))
    m.write("test.nl")
```

```
Traceback (most recent call last):
  File "~/pyoexternal.py", line 143, in <module>
    run()
  File "~/pyoexternal.py", line 81, in run
    m.write("test.nl")
  File "~/anaconda3/envs/pyotest/lib/python3.7/site-packages/pyomo/core/base/block.py", line 1716, in write
    io_options)
  File "~/anaconda3/envs/pyotest/lib/python3.7/site-packages/pyomo/repn/plugins/ampl/ampl_.py", line 378, in __call__
    include_all_variable_bounds=include_all_variable_bounds)
  File "~/anaconda3/envs/pyotest/lib/python3.7/site-packages/pyomo/repn/plugins/ampl/ampl_.py", line 1587, in _print_model_NL
    wrapped_repn.repn.nonlinear_expr)
  File "~/anaconda3/envs/pyotest/lib/python3.7/site-packages/pyomo/repn/plugins/ampl/ampl_.py", line 568, in _print_nonlinear_terms_NL
    elif arg.is_fixed():
AttributeError: 'int' object has no attribute 'is_fixed'
```

From what I can see, `arg[0]` is an integer counter, and `arg[1]` is a `_VarData` instance in this case.
I have the same problem. A simple python external function does not work.

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
@@ -556,6 +556,8 @@ def _print_nonlinear_terms_NL(self, exp):
                 for arg in exp.args:
                     if isinstance(arg, str):
                         OUTPUT.write(string_arg_str % (len(arg), arg))
+                    elif type(arg) in native_numeric_types:
+                        self._print_nonlinear_terms_NL(arg)
                     elif arg.is_fixed():
                         self._print_nonlinear_terms_NL(arg())
                     else:


Test patch:
diff --git a/pyomo/repn/tests/ampl/external_expression_constant.nl.baseline b/pyomo/repn/tests/ampl/external_expression_constant.nl.baseline
new file mode 100644
--- /dev/null
+++ b/pyomo/repn/tests/ampl/external_expression_constant.nl.baseline
@@ -0,0 +1,23 @@
+g3 1 1 0	# problem unknown
+ 1 0 1 0 0 	# vars, constraints, objectives, ranges, eqns
+ 0 1 0 0 0 0	# nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb
+ 0 0	# network constraints: nonlinear, linear
+ 0 1 0 	# nonlinear vars in constraints, objectives, both
+ 0 1 0 1	# linear network variables; functions; arith, flags
+ 0 0 0 0 0 	# discrete variables: binary, integer, nonlinear (b,c,o)
+ 0 1 	# nonzeros in Jacobian, obj. gradient
+ 1 1	# max name lengths: constraints, variables
+ 0 0 0 0 0	# common exprs: b,c,o,c1,o1
+F0 1 -1 gsl_hypot
+O0 0	#o
+f0 2	#hypot
+n3
+v0	#y
+x1	# initial guess
+0 4
+r	#0 ranges (rhs's)
+b	#1 bounds (on variables)
+2 0
+k0	#intermediate Jacobian column lengths
+G0 1
+0 0
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
@@ -109,6 +109,27 @@ def _external_model(self):
         self.assertAlmostEqual(value(m.o), 25.0, 7)
         return m
 
+    def test_external_expression_constant(self):
+        DLL = find_GSL()
+        if not DLL:
+            self.skipTest("Could not find the amplgsl.dll library")
+
+        m = ConcreteModel()
+        m.y = Var(initialize=4, bounds=(0,None))
+        m.hypot = ExternalFunction(library=DLL, function="gsl_hypot")
+        m.o = Objective(expr=m.hypot(3, m.y))
+        self.assertAlmostEqual(value(m.o), 5.0, 7)
+
+        baseline_fname, test_fname = self._get_fnames()
+        self._cleanup(test_fname)
+        m.write(test_fname, format='nl',
+                    io_options={'symbolic_solver_labels':True})
+        self.assertTrue(cmp(
+            test_fname,
+            baseline_fname),
+            msg="Files %s and %s differ" % (test_fname, baseline_fname))
+        self._cleanup(test_fname)
+
     def test_external_expression_variable(self):
         m = self._external_model()