Repository: Pyomo/pyomo

Issue:
Bug in sensitivity toolbox - not updating expression values
Here is a MWE adapted from ``parameter.py`` (example:
```
from pyomo.environ import * 
from pyomo.contrib.sensitivity_toolbox.sens import sipopt

def create_model():
    m = ConcreteModel()
    
    m.x1 = Var(initialize = 0.15, within=NonNegativeReals)
    m.x2 = Var(initialize = 0.15, within=NonNegativeReals)
    m.x3 = Var(initialize = 0.0, within=NonNegativeReals)
    
    m.eta1 = Param(initialize=4.5,mutable=True)
    m.eta2 = Param(initialize=1.0,mutable=True)
    
    m.const1 = Constraint(expr=6*m.x1+3*m.x2+2*m.x3-m.eta1 ==0)
    m.const2 = Constraint(expr=m.eta2*m.x1+m.x2-m.x3-1 ==0)
    m.cost = Objective(expr=m.x1**2+m.x2**2+m.x3**2)
    
    return m 


if __name__=='__main__':
    m = create_model()    

    m.perturbed_eta1 = Param(initialize = 4.0)
    m.perturbed_eta2 = Param(initialize = 1.0)


    m_sipopt = sipopt(m,[m.eta1,m.eta2],
                        [m.perturbed_eta1,m.perturbed_eta2],
                        streamSoln=True)
    
    print("\nOriginal parameter values:")
    print("\teta1 =",m.eta1())
    print("\teta2 =",m.eta2())
    
    print("Solution with the original parameter values:")
    print("\tObjective =",value(m.cost))
    print("\tx1 =",m.x1())
    print("\tx2 =",m.x2())
    print("\tx3 =",m.x3())
    
    print("\nNew parameter values:")
    print("\teta1 =",m_sipopt.perturbed_eta1())
    print("\teta2 =",m_sipopt.perturbed_eta2())
    
    print("(Approximate) solution with the new parameter values:")
    print("\tObjective =",value(m_sipopt.cost))
    print("\tx1 =",m_sipopt.x1())
    print("\tx2 =",m_sipopt.x2())
    print("\tx3 =",m_sipopt.x3())
```

The problem: The value stored in `m.cost` appears to be at the initial point, not the optimal solution.


Discussion:

Resolving patch:
diff --git a/pyomo/contrib/sensitivity_toolbox/examples/parameter.py b/pyomo/contrib/sensitivity_toolbox/examples/parameter.py
--- a/pyomo/contrib/sensitivity_toolbox/examples/parameter.py
+++ b/pyomo/contrib/sensitivity_toolbox/examples/parameter.py
@@ -13,11 +13,13 @@
 # Original implmentation by Hans Pirany is in pyomo/examples/pyomo/suffixes
 #
 
-
+from __future__ import print_function
 from pyomo.environ import * 
 from pyomo.contrib.sensitivity_toolbox.sens import sipopt
 
 def create_model():
+    ''' Create a concrete Pyomo model for this example
+    '''
     m = ConcreteModel()
     
     m.x1 = Var(initialize = 0.15, within=NonNegativeReals)
@@ -31,11 +33,19 @@ def create_model():
     m.const2 = Constraint(expr=m.eta2*m.x1+m.x2-m.x3-1 ==0)
     m.cost = Objective(expr=m.x1**2+m.x2**2+m.x3**2)
     
-    
     return m 
 
-
-if __name__=='__main__':
+def run_example(print_flag=True):
+    '''
+    Execute the example
+    
+    Arguments:
+        print_flag: Toggle on/off printing
+    
+    Returns:
+        sln_dict: Dictionary containing solution (used for automated testing)
+    
+    '''
     m = create_model()    
 
     m.perturbed_eta1 = Param(initialize = 4.0)
@@ -46,20 +56,66 @@ def create_model():
                         [m.perturbed_eta1,m.perturbed_eta2],
                         streamSoln=True)
     
-    print("\nOriginal parameter values:")
-    print("\teta1 =",m.eta1())
-    print("\teta2 =",m.eta2())
+
     
-    print("Solution with the original parameter values:")
-    print("\tx1 =",m.x1())
-    print("\tx2 =",m.x2())
-    print("\tx3 =",m.x3())
+    if print_flag:
+        print("\nOriginal parameter values:")
+        print("\teta1 =",m.eta1())
+        print("\teta2 =",m.eta2())
     
-    print("\nNew parameter values:")
-    print("\teta1 =",m_sipopt.perturbed_eta1())
-    print("\teta2 =",m_sipopt.perturbed_eta2())
+        print("Initial point:")
+        print("\tObjective =",value(m.cost))
+        print("\tx1 =",m.x1())
+        print("\tx2 =",m.x2())
+        print("\tx3 =",m.x3())
+        
+        print("Solution with the original parameter values:")
+        print("\tObjective =",m_sipopt.cost())
+        print("\tx1 =",m_sipopt.x1())
+        print("\tx2 =",m_sipopt.x2())
+        print("\tx3 =",m_sipopt.x3())
     
-    print("(Approximate) solution with the new parameter values:")
-    print("\tx1 =",m_sipopt.x1())
-    print("\tx2 =",m_sipopt.x2())
-    print("\tx3 =",m_sipopt.x3())
+        print("\nNew parameter values:")
+        print("\teta1 =",m_sipopt.perturbed_eta1())
+        print("\teta2 =",m_sipopt.perturbed_eta2())
+    
+    # This highlights one limitation of sipopt. It will only return the
+    # perturbed solution. The user needs to calculate relevant values such as
+    # the objective or expressions
+    x1 = m_sipopt.sens_sol_state_1[m_sipopt.x1]
+    x2 = m_sipopt.sens_sol_state_1[m_sipopt.x2]
+    x3 = m_sipopt.sens_sol_state_1[m_sipopt.x3]
+    obj = x1**2 + x2**2 + x3**2
+    
+    if print_flag:
+        print("(Approximate) solution with the new parameter values:")
+        print("\tObjective =",obj)
+        print("\tx1 =",m_sipopt.sens_sol_state_1[m_sipopt.x1])
+        print("\tx2 =",m_sipopt.sens_sol_state_1[m_sipopt.x2])
+        print("\tx3 =",m_sipopt.sens_sol_state_1[m_sipopt.x3])
+    
+    # Save the results in a dictionary.
+    # This is optional and makes automated testing convenient.
+    # This code is not important for a Minimum Working Example (MWE) of sipopt
+    d = dict()
+    d['eta1'] = m.eta1()
+    d['eta2'] = m.eta2()
+    d['x1_init'] = m.x1()
+    d['x2_init'] = m.x2()
+    d['x3_init'] = m.x3()
+    d['x1_sln'] = m_sipopt.x1()
+    d['x2_sln'] = m_sipopt.x2()
+    d['x3_sln'] = m_sipopt.x3()
+    d['cost_sln'] = m_sipopt.cost()
+    d['eta1_pert'] = m_sipopt.perturbed_eta1()
+    d['eta2_pert'] = m_sipopt.perturbed_eta2()
+    d['x1_pert'] = x1
+    d['x2_pert'] = x2
+    d['x3_pert'] = x3
+    d['cost_pert'] = obj
+    
+    return d
+
+
+if __name__=='__main__':
+    d = run_example()


Test patch:
diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py
--- a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py
+++ b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py
@@ -351,6 +351,22 @@ def test_constraintSub(self):
                        len(list(identify_variables(
                                 m_sipopt._sipopt_data.constList[2].body))) == 2)
 
+    # Test example `parameter.py`
+    @unittest.skipIf(not opt.available(False), "ipopt_sens is not available")
+    def test_parameter_example(self):
+    
+        from pyomo.contrib.sensitivity_toolbox.examples.parameter import run_example
+        d = run_example()
+        
+        d_correct = {'eta1':4.5, 'eta2':1.0, 'x1_init':0.15, 'x2_init':0.15, 'x3_init':0.0,
+            'cost_sln':0.5, 'x1_sln':0.5, 'x2_sln':0.5, 'x3_sln':0.0, 'eta1_pert':4.0,
+            'eta2_pert':1.0, 'x1_pert':0.3333333,'x2_pert':0.6666667,'x3_pert':0.0,
+            'cost_pert':0.55555556}
+        
+        for k in d_correct.keys():
+            # Check each element of the 'correct' dictionary against the returned 
+            # dictionary to 3 decimal places
+            self.assertAlmostEqual(d[k],d_correct[k],3)
 
 if __name__=="__main__":
     unittest.main()