Repository: Pyomo/pyomo

Issue:
DAE: expand blocks indexed by a ContinusSet
We need to update `pyomo.dae.misc.update_contset_indexed_component()` to also expand `Block`s that are indexed by `ContinuousSet`s.  This is an issue with models like a packed column whose length is specified as a continuous set.  Things like the physical properties for each "stage" (finite element) will need to be expanded.

Reported by @eslickj and @andrewlee94.


Discussion:
related to #52

Resolving patch:
diff --git a/pyomo/dae/misc.py b/pyomo/dae/misc.py
--- a/pyomo/dae/misc.py
+++ b/pyomo/dae/misc.py
@@ -12,9 +12,10 @@
 from pyomo.core import *
 from pyomo.core.base.indexed_component import IndexedComponent
 from pyomo.core.base.misc import apply_indexed_rule
+from pyomo.core.base.block import _BlockData, IndexedBlock
 from pyomo.dae import *
 
-from six import iterkeys, itervalues
+from six import iterkeys, itervalues, iteritems
 
 logger = logging.getLogger('pyomo.core')
 
@@ -98,12 +99,8 @@ def update_contset_indexed_component(comp):
     Update any model components which are indexed by a ContinuousSet
     that has changed
     """
-    # FIXME: This implementation is a hack until Var and Constraint get
-    # moved over to Indexed_Component. The update methods below are
-    # roughly what the '_default' method in Var and Constraint should
-    # do when they get reimplemented.
 
-    # Additionally, this implemenation will *NOT* check for or update
+    # This implemenation will *NOT* check for or update
     # components which use a ContinuousSet implicitly. ex) an
     # objective function which iterates through a ContinuousSet and
     # sums the squared error.  If you use a ContinuousSet implicitly
@@ -112,6 +109,15 @@ def update_contset_indexed_component(comp):
 
     if comp.type() is Suffix:
         return
+    
+    # Params indexed by a ContinuousSet should include an initialize
+    # and/or default rule which will be called automatically when the
+    # parameter value at a new point in the ContinuousSet is
+    # requested. Therefore, no special processing is required for
+    # Params.
+    if comp.type() is Param:
+        return
+
     if comp.dim() == 1:
         if comp._index.type() == ContinuousSet:
             if comp._index.get_changed():
@@ -121,6 +127,18 @@ def update_contset_indexed_component(comp):
                     _update_constraint(comp)
                 elif comp.type() == Expression:
                     _update_expression(comp)
+                elif type(comp) is IndexedBlock: # Only catch Blocks and not components derived
+                    _update_block(comp)          # from Blocks
+                elif isinstance(comp, Piecewise):
+                    _update_piecewise(comp)
+                else:
+                    raise TypeError("Found component %s of type %s indexed "\
+                        "by a ContinuousSet. Components of this type are "\
+                        "not currently supported by the automatic "\
+                        "discretization transformation in pyomo.dae. "\
+                        "Try adding the component to the model "\
+                        "after discretizing. Alert the pyomo developers "\
+                        "for more assistance." %(str(comp),comp.type()))
     elif comp.dim() > 1:
         indexset = comp._implicit_subsets
 
@@ -132,11 +150,23 @@ def update_contset_indexed_component(comp):
                     _update_constraint(comp)
                 elif comp.type() == Expression:
                     _update_expression(comp)
+                elif type(comp) is IndexedBlock: # Only catch Blocks and not components derived
+                    _update_block(comp)          # from Blocks
+                elif isinstance(comp, Piecewise):
+                    _update_piecewise(comp)
+                else:
+                    raise TypeError("Found component %s of type %s indexed "\
+                        "by a ContinuousSet. Components of this type are "\
+                        "not currently supported by the automatic "\
+                        "discretization transformation in pyomo.dae. "\
+                        "Try adding the component to the model "\
+                        "after discretizing. Alert the pyomo developers "\
+                        "for more assistance." %(str(comp),comp.type()))
 
 def _update_var(v):
     """
     This method will construct any additional indices in a variable
-    resulting from the discretization of ContinuousSet
+    resulting from the discretization of a ContinuousSet.
     """
 
     # Note: This is not required it is handled by the _default method on
@@ -150,7 +180,7 @@ def _update_var(v):
 def _update_constraint(con):
     """
     This method will construct any additional indices in a constraint
-    resulting from the discretization.
+    resulting from the discretization of a ContinuousSet.
     """
 
     _rule=con.rule
@@ -162,8 +192,8 @@ def _update_constraint(con):
 
 def _update_expression(expre):
     """
-    This method will construct any additional indices in a expression
-    resulting from the discretization
+    This method will construct any additional indices in an expression
+    resulting from the discretization of a ContinuousSet.
     """
     _rule=expre._init_rule
     _parent=expre._parent()
@@ -172,6 +202,38 @@ def _update_expression(expre):
             # Code taken from the construct() method of Expression
             expre.add(i,apply_indexed_rule(expre,_rule,_parent,i))
 
+def _update_block(blk):
+    """
+    This method will construct any additional indices in a block
+    resulting from the discretization of a ContinuousSet.
+    """
+    
+    # Code taken from the construct() method of Block
+    missing_idx = set(blk._index)-set(iterkeys(blk._data))
+    for idx in list(missing_idx):
+        _block = blk[idx]
+        obj = apply_indexed_rule(
+            None, blk._rule, _block, idx, blk._options )
+ 
+        if isinstance(obj, _BlockData) and obj is not _block:
+            # If the user returns a block, use their block instead
+            # of the empty one we just created.
+            for c in list(obj.component_objects(descend_into=False)):
+                obj.del_component(c)
+                _block.add_component(c.local_name, c)
+                # transfer over any other attributes that are not components
+            for name, val in iteritems(obj.__dict__):
+                if not hasattr(_block, name) and not hasattr(blk, name):
+                    super(_BlockData, _block).__setattr__(name, val)
+
+def _update_piecewise(pw):
+    """
+    This method will construct any additional indices in a Piecewise
+    object resulting from the discretization of a ContinuousSet.
+    """
+    pw._constructed = False
+    pw.construct()
+
 def create_access_function(var):
     """
     This method returns a function that returns a component by calling


Test patch:
diff --git a/pyomo/dae/tests/test_misc.py b/pyomo/dae/tests/test_misc.py
--- a/pyomo/dae/tests/test_misc.py
+++ b/pyomo/dae/tests/test_misc.py
@@ -84,7 +84,7 @@ def test_generate_collocation_points(self):
         self.assertTrue(t[1] == 0.4)
         self.assertTrue(t[13] == 5.4)
 
-    # test Params indexed only by a differentialset after discretizing
+    # test Params indexed only by a ContinuousSet after discretizing
     def test_discretized_params_single(self):
         m = ConcreteModel()
         m.t = ContinuousSet(bounds=(0,10))
@@ -166,7 +166,7 @@ def _rule3(m,i,j,k):
         
         
     # test update_contset_indexed_component method for Vars with 
-    # single index of the differentialset
+    # single index of the ContinuousSet
     def test_update_contset_indexed_component_vars_single(self):
         m = ConcreteModel()
         m.t = ContinuousSet(bounds=(0,10))
@@ -248,7 +248,7 @@ def _init(m,i,j,k):
         self.assertTrue(value(m.v3[6,1,1]) == 6)
 
     # test update_contset_indexed_component method for Constraints with
-    # single index of the differentialset
+    # single index of the ContinuousSet
     def test_update_contset_indexed_component_constraints_single(self):
         m = ConcreteModel()
         m.t = ContinuousSet(bounds=(0,10))
@@ -259,7 +259,7 @@ def _con1(m,i):
             return m.p[i]*m.v[i] <= 20
         m.con1 = Constraint(m.t, rule=_con1)
         
-        # Rules that iterate over a differentialset implicitly are not updated
+        # Rules that iterate over a ContinuouSet implicitly are not updated
         # after the discretization
         def _con2(m):
             return sum(m.v[i] for i in m.t) >= 0
@@ -335,8 +335,8 @@ def _con3(m,i,ti,ti2,j,k):
         self.assertTrue(value(m.con3[2,0,2,1,1].lower) == None)
         self.assertTrue(value(m.con3[3,2,3,2,2].upper) == 20)                   
  
-    # test update_contset_indexed_component method for Constraints with
-    # single index of the differentialset
+    # test update_contset_indexed_component method for Expression with
+    # single index of the ContinuouSet
     def test_update_contset_indexed_component_expressions_single(self):
         m = ConcreteModel()
         m.t = ContinuousSet(bounds=(0,10))
@@ -347,7 +347,7 @@ def _con1(m,i):
             return m.p[i]*m.v[i]
         m.con1 = Expression(m.t, rule=_con1)
 
-        # Rules that iterate over a differentialset implicitly are not updated
+        # Rules that iterate over a ContinuousSet implicitly are not updated
         # after the discretization
         def _con2(m):
             return sum(m.v[i] for i in m.t)
@@ -364,7 +364,7 @@ def _con2(m):
         self.assertEqual(m.con1[8](), 15)
         self.assertEqual(m.con2(), 10)
 
-    # test update_contset_indexed_component method for Constraints with
+    # test update_contset_indexed_component method for Expressions with
     # multiple indices
     def test_update_contset_indexed_component_expressions_multiple(self):
         m = ConcreteModel()
@@ -417,6 +417,251 @@ def _con3(m,i,ti,ti2,j,k):
         self.assertEqual(m.con3[2,6,3,1,1](), -3)
         self.assertEqual(m.con3[3,8,2,2,2](), -6)
 
+    # test update_contset_indexed_component method for Blocks 
+    # indexed by a ContinuousSet
+    def test_update_contset_indexed_component_block_single(self):
+        model = ConcreteModel()
+        model.t = ContinuousSet(bounds=(0,10))
+
+        def _block_rule(b, t):
+            m = b.model()
+    
+            b.s1 = Set(initialize=['A1','A2','A3'])
+            def _init(m,j):
+                return j*2
+            b.p1 = Param(m.t, default=_init)
+            b.v1 = Var(m.t, initialize=5)
+            b.v2 = Var(m.t,initialize=2)
+            b.v3 = Var(m.t, b.s1, initialize=1)
+
+            def _con1(_b,ti):
+                return _b.v1[ti]*_b.p1[ti] == _b.v1[t]**2
+            b.con1 = Constraint(m.t,rule=_con1)
+
+            def _con2(_b,i,ti):
+                return _b.v2[ti] - _b.v3[ti,i] +_b.p1[ti]
+            b.con2 = Expression(b.s1,m.t,rule=_con2)
+    
+        model.blk = Block(model.t, rule=_block_rule)
+     
+        self.assertTrue(len(model.blk),2)
+
+        generate_finite_elements(model.t,5)
+        update_contset_indexed_component(model.blk)
+
+        self.assertEqual(len(model.blk),6)
+        self.assertEqual(len(model.blk[10].con1),2)
+        self.assertEqual(len(model.blk[2].con1),6)
+        self.assertEqual(len(model.blk[10].v2),2)
+
+        self.assertEqual(model.blk[2].p1[2], 4)
+        self.assertEqual(model.blk[8].p1[6], 12)
+        
+        self.assertEqual(model.blk[4].con1[4](), 15)
+        self.assertEqual(model.blk[6].con1[8](), 55)
+
+        self.assertEqual(model.blk[0].con2['A1',10](), 21)
+        self.assertEqual(model.blk[4].con2['A2',6](), 13)
+
+    # test update_contset_indexed_component method for Blocks with
+    # multiple indices
+    def test_update_contset_indexed_component_block_multiple(self):
+        model = ConcreteModel()
+        model.t = ContinuousSet(bounds=(0,10))
+        model.s1 = Set(initialize=['A','B','C'])
+        model.s2 = Set(initialize=[('x1','x1'),('x2','x2')])
+
+        def _block_rule(b, t, s1):
+            m = b.model()
+            
+            def _init(m,i,j):
+                return j*2
+            b.p1 = Param(m.s1,m.t,mutable=True,default=_init)
+            b.v1 = Var(m.s1, m.t, initialize=5)
+            b.v2 = Var(m.s2,m.t,initialize=2)
+            b.v3 = Var(m.t,m.s2, initialize=1)
+
+            def _con1(_b,si,ti):
+                return _b.v1[si,ti]*_b.p1[si,ti] == _b.v1[si,t]**2
+            b.con1 = Constraint(m.s1,m.t,rule=_con1)
+
+            def _con2(_b,i,j,ti):
+                return _b.v2[i,j,ti] - _b.v3[ti,i,j] +_b.p1['A',ti]
+            b.con2 = Expression(m.s2,m.t,rule=_con2)
+    
+        model.blk = Block(model.t, model.s1, rule=_block_rule)
+        
+        self.assertTrue(len(model.blk),6)
+
+        generate_finite_elements(model.t,5)
+        update_contset_indexed_component(model.blk)
+
+        self.assertEqual(len(model.blk),18)
+        self.assertEqual(len(model.blk[10,'C'].con1),6)
+        self.assertEqual(len(model.blk[2,'B'].con1),18)
+        self.assertEqual(len(model.blk[10,'C'].v2),4)
+
+        self.assertEqual(model.blk[2,'A'].p1['A',2], 4)
+        self.assertEqual(model.blk[8,'C'].p1['B',6], 12)
+        
+        self.assertEqual(model.blk[4,'B'].con1['B',4](), 15)
+        self.assertEqual(model.blk[6,'A'].con1['C',8](), 55)
+
+        self.assertEqual(model.blk[0,'A'].con2['x1','x1',10](), 21)
+        self.assertEqual(model.blk[4,'C'].con2['x2','x2',6](), 13)
+
+    # test update_contset_indexed_component method for Blocks 
+    # indexed by a ContinuousSet. Block rule returns new block
+    def test_update_contset_indexed_component_block_single2(self):
+        model = ConcreteModel()
+        model.t = ContinuousSet(bounds=(0,10))
+
+        def _block_rule(_b_, t):
+            m = _b_.model()
+            b = Block()
+
+            b.s1 = Set(initialize=['A1','A2','A3'])
+            def _init(m,j):
+                return j*2
+            b.p1 = Param(m.t, default=_init)
+            b.v1 = Var(m.t, initialize=5)
+            b.v2 = Var(m.t,initialize=2)
+            b.v3 = Var(m.t, b.s1, initialize=1)
+
+            def _con1(_b,ti):
+                return _b.v1[ti]*_b.p1[ti] == _b.v1[t]**2
+            b.con1 = Constraint(m.t,rule=_con1)
+
+            def _con2(_b,i,ti):
+                return _b.v2[ti] - _b.v3[ti,i] +_b.p1[ti]
+            b.con2 = Expression(b.s1,m.t,rule=_con2)
+            return b
+    
+        model.blk = Block(model.t, rule=_block_rule)
+     
+        self.assertTrue(len(model.blk),2)
+
+        generate_finite_elements(model.t,5)
+        update_contset_indexed_component(model.blk)
+
+        self.assertEqual(len(model.blk),6)
+        self.assertEqual(len(model.blk[10].con1),2)
+        self.assertEqual(len(model.blk[2].con1),6)
+        self.assertEqual(len(model.blk[10].v2),2)
+
+        self.assertEqual(model.blk[2].p1[2], 4)
+        self.assertEqual(model.blk[8].p1[6], 12)
+        
+        self.assertEqual(model.blk[4].con1[4](), 15)
+        self.assertEqual(model.blk[6].con1[8](), 55)
+
+        self.assertEqual(model.blk[0].con2['A1',10](), 21)
+        self.assertEqual(model.blk[4].con2['A2',6](), 13)
+
+    # test update_contset_indexed_component method for Blocks with
+    # multiple indices. Block rule returns new block
+    def test_update_contset_indexed_component_block_multiple2(self):
+        model = ConcreteModel()
+        model.t = ContinuousSet(bounds=(0,10))
+        model.s1 = Set(initialize=['A','B','C'])
+        model.s2 = Set(initialize=[('x1','x1'),('x2','x2')])
+
+        def _block_rule(_b_, t, s1):
+            m = _b_.model()
+            b = Block()
+
+            def _init(m,i,j):
+                return j*2
+            b.p1 = Param(m.s1,m.t,mutable=True,default=_init)
+            b.v1 = Var(m.s1, m.t, initialize=5)
+            b.v2 = Var(m.s2,m.t,initialize=2)
+            b.v3 = Var(m.t,m.s2, initialize=1)
+
+            def _con1(_b,si,ti):
+                return _b.v1[si,ti]*_b.p1[si,ti] == _b.v1[si,t]**2
+            b.con1 = Constraint(m.s1,m.t,rule=_con1)
+
+            def _con2(_b,i,j,ti):
+                return _b.v2[i,j,ti] - _b.v3[ti,i,j] +_b.p1['A',ti]
+            b.con2 = Expression(m.s2,m.t,rule=_con2)
+            return b
+    
+        model.blk = Block(model.t, model.s1, rule=_block_rule)
+        
+        self.assertTrue(len(model.blk),6)
+
+        generate_finite_elements(model.t,5)
+        update_contset_indexed_component(model.blk)
+
+        self.assertEqual(len(model.blk),18)
+        self.assertEqual(len(model.blk[10,'C'].con1),6)
+        self.assertEqual(len(model.blk[2,'B'].con1),18)
+        self.assertEqual(len(model.blk[10,'C'].v2),4)
+
+        self.assertEqual(model.blk[2,'A'].p1['A',2], 4)
+        self.assertEqual(model.blk[8,'C'].p1['B',6], 12)
+        
+        self.assertEqual(model.blk[4,'B'].con1['B',4](), 15)
+        self.assertEqual(model.blk[6,'A'].con1['C',8](), 55)
+
+        self.assertEqual(model.blk[0,'A'].con2['x1','x1',10](), 21)
+        self.assertEqual(model.blk[4,'C'].con2['x2','x2',6](), 13)
+
+
+    # test update_contset_indexed_component method for Piecewise
+    # component indexed by a ContinuousSet
+    def test_update_contset_indexed_component_piecewise_single(self):
+        x = [0.0, 1.5, 3.0, 5.0]
+        y = [1.1, -1.1, 2.0, 1.1]
+        
+        model = ConcreteModel()
+        model.t = ContinuousSet(bounds=(0,10))
+
+        model.x = Var(model.t, bounds=(min(x),max(x)))
+        model.y = Var(model.t)
+
+        model.fx = Piecewise(model.t, 
+                             model.y, model.x,
+                             pw_pts=x,
+                             pw_constr_type='EQ',
+                             f_rule=y)
+        
+        self.assertEqual(len(model.fx), 2)
+
+        generate_finite_elements(model.t,5)
+        update_contset_indexed_component(model.fx)
+
+        self.assertEqual(len(model.fx), 6)
+        self.assertEqual(len(model.fx[2].SOS2_constraint), 3)
+
+
+    # test update_contset_indexed_component method for Piecewise 
+    # component with multiple indices
+    def test_update_contset_indexed_component_piecewise_multiple(self):
+        x = [0.0, 1.5, 3.0, 5.0]
+        y = [1.1, -1.1, 2.0, 1.1]
+        
+        model = ConcreteModel()
+        model.t = ContinuousSet(bounds=(0,10))
+        model.s = Set(initialize=['A','B','C'])
+
+        model.x = Var(model.s, model.t, bounds=(min(x),max(x)))
+        model.y = Var(model.s, model.t)
+
+        model.fx = Piecewise(model.s, model.t, 
+                             model.y, model.x,
+                             pw_pts=x,
+                             pw_constr_type='EQ',
+                             f_rule=y)
+        
+        self.assertEqual(len(model.fx), 6)
+
+        generate_finite_elements(model.t,5)
+        update_contset_indexed_component(model.fx)
+
+        self.assertEqual(len(model.fx), 18)
+        self.assertEqual(len(model.fx['A',2].SOS2_constraint), 3)
+
     # test update_contset_indexed_component method for other components
     def test_update_contset_indexed_component_other(self):
         m = ConcreteModel()
@@ -432,6 +677,37 @@ def _obj(m):
         update_contset_indexed_component(m.junk)
         update_contset_indexed_component(m.s)
         update_contset_indexed_component(m.obj)
+
+    # test unsupported components indexed by a single ContinuousSet
+    def test_update_contset_indexed_component_unsupported_single(self):
+        m = ConcreteModel()
+        m.t = ContinuousSet(bounds=(0,10))
+        m.s = Set(m.t)
+        generate_finite_elements(m.t,5)
+
+        try :
+            update_contset_indexed_component(m.s)
+            self.fail("Expected TypeError because Set is not a component "\
+                      "that supports indexing by a ContinuousSet")
+
+        except TypeError:
+            pass
+
+    # test unsupported components indexed by multiple sets
+    def test_update_contset_indexed_component_unsupported_multiple(self):
+        m = ConcreteModel()
+        m.t = ContinuousSet(bounds=(0,10))
+        m.i = Set(initialize=[1,2,3])
+        m.s = Set(m.i, m.t)
+        generate_finite_elements(m.t,5)
+
+        try :
+            update_contset_indexed_component(m.s)
+            self.fail("Expected TypeError because Set is not a component "\
+                      "that supports indexing by a ContinuousSet")
+
+        except TypeError:
+            pass  
         
     # test add_equality_constraints method
     # def test_add_equality_constraints(self):