Repository: Pyomo/pyomo

Issue:
Var fix implementation
We have an interesting implementation of variable fixing to support both `var.fix()` and `var.fix(value)`:
https://github.com/Pyomo/pyomo/blob/ccde78cab8cabaf8822b66f741b63bd2797777d0/pyomo/core/base/var.py#L894

However, does it ever make sense to `var.fix(None)`? If not, it would be far more clear to implement as an optional argument.


Discussion:
My opinion is that it may or may not makes sense for a user to do this. However, it is very easy for a variable to be put into this state (fixed=True, value=None) by setting those attributes.

If you wanted to put together a pull request that preserves the current behavior, but replaces the *args syntax with an optional keyword argument, you could just as easily use a class as the default value and do:
```python
class _noarg(object):
    pass

def fix(self, value=_noarg):
    if value is not _noarg:
        self.value = value
    self.fixed = True
```
This will still support the `fix(None)` case, in case anyone is using it.
I guess the question is: Would this PR be worth your time?
I certainly like the idea of moving away from `fix(self, *arg)` - I guarantee that we are not checking for nearly enough edge cases (like `m.x.fix(5, 2)`?).

@ghackebeil's proposal is the right way to go (and we have flavors of the `_noarg` or `NotSpecified` class littered all over the codebase).

I believe all implementations are correctly checking that at most one arg is provided, so this change would really be just about stylistic preference.

> On Dec 11, 2019, at 1:36 PM, John Siirola <notifications@github.com> wrote:
> 
> ﻿
> I certainly like the idea of moving away from fix(self, *arg) - I guarantee that we are not checking for nearly enough edge cases (like m.x.fix(5, 2)?).
> 
> @ghackebeil's proposal is the right way to go (and we have flavors of the _noarg or NotSpecified class littered all over the codebase).
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub, or unsubscribe.

@ghackebeil is correct that the current implementation correctly checks for the multi-argument case, but there was non-zero confusion when I skimmed over the code (trying to implement something else). It's a mostly-invisible change, so I think I'll put together a small PR to adjust it.

In that case I would recommend making the keyword name “value” instead of “val”.

Would you mind make this change the the variable in kernel as well?

> On Dec 11, 2019, at 5:18 PM, Qi Chen <notifications@github.com> wrote:
> 
> ﻿
> @ghackebeil is correct that the current implementation correctly checks for the multi-argument case, but there was non-zero confusion when I skimmed over the code (trying to implement something else). It's a mostly-invisible change, so I think I'll put together a small PR to adjust it.
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub, or unsubscribe.


Resolving patch:
diff --git a/pyomo/common/modeling.py b/pyomo/common/modeling.py
--- a/pyomo/common/modeling.py
+++ b/pyomo/common/modeling.py
@@ -10,6 +10,7 @@
 
 from random import random
 
+
 def randint(a,b):
     """Our implementation of random.randint.
 
@@ -20,6 +21,7 @@ def randint(a,b):
     version of randint()."""
     return int((b-a+1)*random())
 
+
 def unique_component_name(instance, name):
     # test if this name already exists in model. If not, we're good. 
     # Else, we add random numbers until it doesn't
@@ -31,3 +33,14 @@ def unique_component_name(instance, name):
             return name
         else:
             name += str(randint(0,9))
+
+
+"""
+Object to be used to indicate that an optional argument
+was not specified, if `None` may be ambiguous. Usage:
+
+  >>> def foo(value=NoArgumentGiven):
+  >>>     if value is NoArgumentGiven:
+  >>>         pass  # no argument was provided to `value`
+"""
+NoArgumentGiven = object()
diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py
--- a/pyomo/core/base/var.py
+++ b/pyomo/core/base/var.py
@@ -13,6 +13,7 @@
 import logging
 from weakref import ref as weakref_ref
 
+from pyomo.common.modeling import NoArgumentGiven
 from pyomo.common.timing import ConstructionTimer
 from pyomo.core.base.numvalue import NumericValue, value, is_fixed
 from pyomo.core.base.set_types import BooleanSet, IntegerSet, RealSet, Reals
@@ -252,7 +253,7 @@ def setub(self, val):
         """
         raise NotImplementedError
 
-    def fix(self, *val):
+    def fix(self, value=NoArgumentGiven):
         """
         Set the fixed indicator to True. Value argument is optional,
         indicating the variable should be fixed at its current value.
@@ -440,16 +441,14 @@ def setub(self, val):
                 "parameters"
                 % (type(val),))
 
-    def fix(self, *val):
+    def fix(self, value=NoArgumentGiven):
         """
         Set the fixed indicator to True. Value argument is optional,
         indicating the variable should be fixed at its current value.
         """
         self.fixed = True
-        if len(val) == 1:
-            self.value = val[0]
-        elif len(val) > 1:
-            raise TypeError("fix expected at most 1 arguments, got %d" % (len(val)))
+        if value is not NoArgumentGiven:
+            self.value = value
 
     def unfix(self):
         """Sets the fixed indicator to False."""
@@ -891,13 +890,13 @@ def setub(self, val):
             "is currently nothing to set)."
             % (self.name))
 
-    def fix(self, *val):
+    def fix(self, value=NoArgumentGiven):
         """
         Set the fixed indicator to True. Value argument is optional,
         indicating the variable should be fixed at its current value.
         """
         if self._constructed:
-            return _GeneralVarData.fix(self, *val)
+            return _GeneralVarData.fix(self, value)
         raise ValueError(
             "Fixing variable '%s' "
             "before the Var has been constructed (there "
@@ -933,13 +932,13 @@ def setub(self, val):
         for vardata in itervalues(self):
             vardata.setub(val)
 
-    def fix(self, *val):
+    def fix(self, value=NoArgumentGiven):
         """
         Set the fixed indicator to True. Value argument is optional,
         indicating the variable should be fixed at its current value.
         """
         for vardata in itervalues(self):
-            vardata.fix(*val)
+            vardata.fix(value=value)
 
     def unfix(self):
         """Sets the fixed indicator to False."""
diff --git a/pyomo/core/kernel/variable.py b/pyomo/core/kernel/variable.py
--- a/pyomo/core/kernel/variable.py
+++ b/pyomo/core/kernel/variable.py
@@ -7,7 +7,7 @@
 #  rights in this software.
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
-
+from pyomo.common.modeling import NoArgumentGiven
 from pyomo.core.expr.numvalue import (NumericValue,
                                       is_numeric_data,
                                       value)
@@ -108,17 +108,15 @@ def bounds(self):
     def bounds(self, bounds_tuple):
         self.lb, self.ub = bounds_tuple
 
-    def fix(self, *val):
+    def fix(self, value=NoArgumentGiven):
         """
         Fix the variable. Sets the fixed indicator to
         :const:`True`. An optional value argument will
         update the variable's value before fixing.
         """
-        if len(val) == 1:
-            self.value = val[0]
-        elif len(val) > 1:
-            raise TypeError("fix expected at most 1 arguments, "
-                            "got %d" % (len(val)))
+        if value is not NoArgumentGiven:
+            self.value = value
+
         self.fixed = True
 
     def unfix(self):


Test patch:
diff --git a/pyomo/core/tests/unit/test_var.py b/pyomo/core/tests/unit/test_var.py
--- a/pyomo/core/tests/unit/test_var.py
+++ b/pyomo/core/tests/unit/test_var.py
@@ -216,13 +216,13 @@ def test_fix_indexed(self):
         self.instance.y[1].free()
         self.assertEqual(self.instance.y[1].value, None)
         self.assertEqual(self.instance.y[1].fixed, False)
-        self.instance.y[1].fix(1)
+        self.instance.y[1].fix(value=1)
         self.assertEqual(self.instance.y[1].value, 1)
         self.assertEqual(self.instance.y[1].fixed, True)
         self.instance.y[1].unfix()
         self.assertEqual(self.instance.y[1].value, 1)
         self.assertEqual(self.instance.y[1].fixed, False)
-        self.instance.y[1].fix(None)
+        self.instance.y[1].fix(value=None)
         self.assertEqual(self.instance.y[1].value, None)
         self.assertEqual(self.instance.y[1].fixed, True)