Repository: Pyomo/pyomo

Issue:
Implement a simple Kirchoff law for Extensive port in pyomo.network
Hello,
I am experimenting the pyomo network for hierarchical modelling. First, thanks a lot for your contributions. This is a very useful tool !
I have some hard time understanding the philosophy behind the Port.Extensive rule when more than one arc is expanded.

Let's take a simple example with 3 blocks, one "source", b1, connected to 2 "destinations", b2 and b3. Let's assume that each destination is connected to the source through a port that expose an extensive variable, i.e. a flow.

Here a simple code to do this : 

```
from pyomo.environ import *
from pyomo.network import *
from pyomo.dae import *

m = ConcreteModel()

m.b1 = Block()
m.b2 = Block()
m.b3 = Block()

def source_block(b):
    b.t = ContinuousSet(bounds=(0, 1))
    b.p_out = Var(b.t)
    b.outlet = Port(initialize={'p': (b.p_out, Port.Extensive)})

def load_block(b):
    b.t = ContinuousSet(bounds=(0, 1))
    b.p_in = Var(b.t)
    b.inlet = Port(initialize={'p': (b.p_in, Port.Extensive)})

source_block(m.b1)
load_block(m.b2)
load_block(m.b3)

m.b12 = Arc(source=m.b1.outlet, destination=m.b2.inlet)
m.b13 = Arc(source=m.b1.outlet, destination=m.b3.inlet)

TransformationFactory('dae.finite_difference').apply_to(m, nfe=1)
TransformationFactory("network.expand_arcs").apply_to(m)

m.pprint()
```
I don't really understand why one would introduce the following variables : `b13_expanded.splitfact` and `b13_expanded.splitfact` the following constraints `m.b12_expanded.p_split` and `m.b13_expanded.p_split` within the new connection blocks. In a general flow  connection, one would only need a Kirchoff law (i.e. sum of flows are equal to 0) without imposing split (the physic will actually take care of this.), i.e. : `b1.p_out[i] == b2.p_in[t] + b3.pin[t]` which is equivalent to the set of generated constraints `b1.outlet_p_outsum`,  `b2.inlet_p_insum` and `b3.inlet_p_insum`. So here is my questions/remarks : 

- Would this Kirchoff law be a new kind of expansion for arcs ?
- Is there an easy way to automatically disable the constraints `b12_expanded.p_split` and `b13_expanded.p_split` ? Or make this behaviour an option ? In the call for Arc for instance.

Thanks again for your contribution and for your help, 
Greetings




Discussion:
If I understand correctly, I believe the behavior you're seeing is necessary for the expansion to be correct. If you were to skip the split fractions and just write an indexed constraint for all of the `p` variables saying the source `p` equals the sum of the dest `p`'s, you would achieve the physical property where the flow is conserved, but since `p` is an indexed variable (in this case over time), there would be no guarantee that each arc contains an equal "portion" of `p` at every time index.

Consider a variable `x` indexed over the set `{1, 2}`. Without `splitfrac` variables, we would get the constraints

```
b1.x[1] == b2.x[1] + b3.x[1]
b1.x[2] == b2.x[2] + b3.x[2]
```

If for example we fix `b1.x[1] = b1.x[2] = 5`, then these constraints allow for the following values

```
b2.x[1] = 3
b3.x[1] = 2
b2.x[2] = 1
b2.x[2] = 4
```

This violates the intention of the extensive rule, since we basically have `x` being split in different portions for its different indexes. That was why split fractions were ([almost](https://pyomo.readthedocs.io/en/latest/modeling_extensions/network.html#pyomo.network.Port.Extensive)) always included, since using them across all of the `_split` constraints guarantees that each destination arc contains an equal portion of every variable entering it.
Hi, thanks for your answer and explanations !
I see your point in introducing this fraction variable and I am sure this allows to deal with real applications in graph optimization. However, I believe this is a particular case. 

For my dynamic physical graph applications, I am looking for a 'classical' flow conservation constraint, without assuming constant split (exactly as you described). This is actually more generic in my perspective. In thermodynamics for instance, the heat-flow variables are deduced from effort variables, or potentials (Temperature in this case). 
As a consequence, considering fixed split fraction would be equivalent to fixing temperature differences for all instant. Fixing this split might be a nice simplification in some cases. But it is a strong hypothesis in the general case, in my point of view. In definitive, I'd like to let ``x` being split in different portions for its different index. 
This behaviour is the same for a large variety of applications : thermal (temperature / heat-flow), fluids (pressure / flow), electric (voltage / current), mechanic (force / velocity) . This kind of graph are known as 'bond-graph'. 

In conclusion, I believe that a "Conservative" rule might be a good enhancement for the network module. What is your position on this ? In your opinion, is this new rule, lets call it Conservative, would be hard to develop ? Could it be a new PR ? 

Greetings

ps: I could not add a label to this issue. Can someone add a pyomo.network label ?
I agree that what you're looking for differs from the purpose that the Extensive rule serves. The desired behavior could be implemented in one of at least two ways. One would be to create a new rule, like Conservative, which could write simpler summation constraints for the variables that get created on the arcs' expanded blocks (referred to as `evar`'s in the code). However, in order to implement the use of "effort variables" in your example, you would (I assume) need to write more than just summations. Moreover, all of your different applications you listed probably have different needs in regards to dictating how flows split, so different procedures would need to be implemented for them.

You could either implement the use of effort variables (or whatever your application needs) within the Conservative rule procedure, having it generate both summations as well as constraints that specify the specific flows, or you could implement that within your block that contains the outlet port. Yet, if you were to implement such constraints on the block, you would need to do so after expanding the arcs since you would presumably need access to the evars which only get created during expansion. Alternatively, you could also break down the complexity further by giving your block multiple outlet ports which contain their own variables, and which are 1-to-1 connected to their downstream ports, and then place constraints on your block that specify the variables in those ports so it basically does the splitting itself without using the arc expander rules. Of course, this option eliminates the convenience of the implicit mixing and splitting rules.

In summary, you can get your desired behavior with a new rule, but this rule would also be a good place to add the use of effort variables or whatever physical attributes dictate flow splits at a given instant. But you might need a different rule for every different relevant application. And if not within that rule, these splitting constraints could be written on the block but it might not be as clean.

The expansion rule framework was left open to allow for the use of custom rules, or a PR can be opened to add the rule to the Port class for wider use. You can look at the code for Equality and Extensive [here](https://github.com/Pyomo/pyomo/blob/6c042537da93146eaef1398aa544bda0e5f85a56/pyomo/network/port.py#L443), and hopefully they are well commented enough to see what went into them and give a shot at writing your own.

Resolving patch:
diff --git a/pyomo/network/port.py b/pyomo/network/port.py
--- a/pyomo/network/port.py
+++ b/pyomo/network/port.py
@@ -2,8 +2,8 @@
 #
 #  Pyomo: Python Optimization Modeling Objects
 #  Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC
-#  Under the terms of Contract DE-NA0003525 with National Technology and 
-#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain 
+#  Under the terms of Contract DE-NA0003525 with National Technology and
+#  Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
 #  rights in this software.
 #  This software is distributed under the 3-clause BSD License.
 #  ___________________________________________________________________________
@@ -30,7 +30,7 @@
     IPyomoScriptModifyInstance, TransformationFactory
 from pyomo.core.kernel.component_map import ComponentMap
 
-from pyomo.network.util import replicate_var
+from pyomo.network.util import create_var, tighten_var_domain
 
 logger = logging.getLogger('pyomo.network')
 
@@ -381,13 +381,23 @@ def _add_from_container(self, port, items):
         if type(items) is dict:
             for key, val in iteritems(items):
                 if type(val) is tuple:
-                    port.add(val[0], key, val[1])
+                    if len(val) == 2:
+                        obj, rule = val
+                        port.add(obj, key, rule)
+                    else:
+                        obj, rule, kwds = val
+                        port.add(obj, key, rule, **kwds)
                 else:
                     port.add(val, key)
         else:
             for val in self._initialize:
                 if type(val) is tuple:
-                    port.add(val[0], rule=val[1])
+                    if len(val) == 2:
+                        obj, rule = val
+                        port.add(obj, rule=rule)
+                    else:
+                        obj, rule, kwds = val
+                        port.add(obj, rule=rule, **kwds)
                 else:
                     port.add(val)
 
@@ -448,29 +458,39 @@ def Equality(port, name, index_set):
             Port._add_equality_constraint(arc, name, index_set)
 
     @staticmethod
-    def Extensive(port, name, index_set, include_splitfrac=False,
+    def Extensive(port, name, index_set, include_splitfrac=None,
             write_var_sum=True):
-        """
-        Arc Expansion procedure for extensive variable properties
+        """Arc Expansion procedure for extensive variable properties
 
         This procedure is the rule to use when variable quantities should
-        be split for outlets and combined for inlets.
-
-        This will first go through every destination of the port and create
-        a new variable on the arc's expanded block of the same index as the
-        current variable being processed. It will also create a splitfrac
-        variable on the expanded block as well. Then it will generate
-        constraints for the new variable that relates it to the port member
-        variable by the split fraction. Following this, an indexed constraint
-        is written that states that the sum of all the new variables equals
-        the parent. However, if `write_var_sum=False` is passed, instead of
-        this last indexed constraint, a single constraint will be written
-        that states the sum of the split fractions equals 1.
-
-        Then, this procedure will go through every source of the port and
-        create a new variable (unless it already exists), and then write
-        a constraint that states the sum of all the incoming new variables
-        must equal the parent variable.
+        be conserved; that is, split for outlets and combined for inlets.
+
+        This will first go through every destination of the port (i.e.,
+        arcs whose source is this Port) and create a new variable on the
+        arc's expanded block of the same index as the current variable
+        being processed to store the amount of the variable that flows
+        over the arc.  For ports that have multiple outgoing arcs, this
+        procedure will create a single splitfrac variable on the arc's
+        expanded block as well. Then it will generate constraints for
+        the new variable that relate it to the port member variable
+        using the split fraction, ensuring that all extensive variables
+        in the Port are split using the same ratio.  The generation of
+        the split fraction variable and constraint can be suppressed by
+        setting the `include_splitfrac` argument to `False`.
+
+        Once all arc-specific variables are created, this
+        procedure will create the "balancing constraint" that ensures
+        that the sum of all the new variables equals the original port
+        member variable. This constraint can be suppressed by setting
+        the `write_var_sum` argument to `False`; in which case, a single
+        constraint will be written that states the sum of the split
+        fractions equals 1.
+
+        Finally, this procedure will go through every source for this
+        port and create a new arc variable (unless it already exists),
+        before generating the balancing constraint that ensures the sum
+        of all the incoming new arc variables equals the original port
+        variable.
 
         Model simplifications:
 
@@ -486,11 +506,12 @@ def Extensive(port, name, index_set, include_splitfrac=False,
             If the port only contains a single Extensive variable, the
             splitfrac variables and the splitting constraints will
             be skipped since they will be unnecessary. However, they
-            can be still be included by passing include_splitfrac=True.
+            can be still be included by passing `include_splitfrac=True`.
 
         .. note::
             If split fractions are skipped, the `write_var_sum=False`
             option is not allowed.
+
         """
         port_parent = port.parent_block()
         out_vars = Port._Split(port, name, index_set,
@@ -520,6 +541,9 @@ def _Combine(port, name, index_set):
             evar = Port._create_evar(port.vars[name], name, eblock, index_set)
             in_vars.append(evar)
 
+        if len(sources) == 1:
+            tighten_var_domain(port.vars[name], in_vars[0], index_set)
+
         # Create constraint: var == sum of evars
         # Same logic as Port._Split
         cname = unique_component_name(port_parent, "%s_%s_insum" %
@@ -535,12 +559,11 @@ def rule(m, *args):
         return in_vars
 
     @staticmethod
-    def _Split(port, name, index_set, include_splitfrac=False,
+    def _Split(port, name, index_set, include_splitfrac=None,
             write_var_sum=True):
         port_parent = port.parent_block()
         var = port.vars[name]
         out_vars = []
-        no_splitfrac = False
         dests = port.dests(active=True)
 
         if not len(dests):
@@ -556,7 +579,8 @@ def _Split(port, name, index_set, include_splitfrac=False,
                         "Cannot fix splitfrac not at 1 for port '%s' with a "
                         "single dest '%s'" % (port.name, dests[0].name))
 
-            no_splitfrac = True
+            if include_splitfrac is not True:
+                include_splitfrac = False
 
             if len(dests[0].destination.sources(active=True)) == 1:
                 # This is a 1-to-1 connection, no need for evar, just equality.
@@ -571,7 +595,7 @@ def _Split(port, name, index_set, include_splitfrac=False,
             evar = Port._create_evar(port.vars[name], name, eblock, index_set)
             out_vars.append(evar)
 
-            if no_splitfrac:
+            if include_splitfrac is False:
                 continue
 
             # Create and potentially initialize split fraction variables.
@@ -606,7 +630,7 @@ def _Split(port, name, index_set, include_splitfrac=False,
                                     "splitfracs, please pass the "
                                     " include_splitfrac=True argument." %
                                     (port.name, arc.name))
-                        no_splitfrac = True
+                        include_splitfrac = False
                         continue
 
                 eblock.splitfrac = Var()
@@ -626,6 +650,9 @@ def rule(m, *args):
             con = Constraint(index_set, rule=rule)
             eblock.add_component(cname, con)
 
+        if len(dests) == 1:
+            tighten_var_domain(port.vars[name], out_vars[0], index_set)
+
         if write_var_sum:
             # Create var total sum constraint: var == sum of evars
             # Need to alphanum port name in case it is indexed.
@@ -640,7 +667,7 @@ def rule(m, *args):
             port_parent.add_component(cname, con)
         else:
             # OR create constraint on splitfrac vars: sum == 1
-            if no_splitfrac:
+            if include_splitfrac is False:
                 raise ValueError(
                     "Cannot choose to write split fraction sum constraint for "
                     "ports with a single destination or a single Extensive "
@@ -680,10 +707,9 @@ def _create_evar(member, name, eblock, index_set):
         # before making a new one.
         evar = eblock.component(name)
         if evar is None:
-            evar = replicate_var(member, name, eblock, index_set)
+            evar = create_var(member, name, eblock, index_set)
         return evar
 
-
 class SimplePort(Port, _PortData):
 
     def __init__(self, *args, **kwd):
@@ -694,4 +720,3 @@ def __init__(self, *args, **kwd):
 class IndexedPort(Port):
     pass
 
-
diff --git a/pyomo/network/util.py b/pyomo/network/util.py
--- a/pyomo/network/util.py
+++ b/pyomo/network/util.py
@@ -11,39 +11,63 @@
 from pyomo.core import Var
 from pyomo.core.base.indexed_component import UnindexedComponent_set
 
-def replicate_var(comp, name, block, index_set=None):
-    """
-    Create a new variable that will have the same indexing set, domain,
-    and bounds as the provided component, and add it to the given block.
-    Optionally pass an index set to use that to build the variable, but
-    this set must be symmetric to comp's index set.
-    """
+def create_var(comp, name, block, index_set=None):
     if index_set is None:
         if comp.is_indexed():
             index_set = comp.index_set()
         else:
             index_set = UnindexedComponent_set
 
-    var_args = {}
-    try:
-        var_args['domain'] = comp.domain
-    except AttributeError:
-        pass
-    try:
-        var_args['bounds'] = comp.bounds
-    except AttributeError:
-        pass
-
-    new_var = Var(index_set, **var_args)
+    new_var = Var(index_set)
     block.add_component(name, new_var)
+    return new_var
+
+def _tighten(src, dest):
+    starting_lb = dest.lb
+    starting_ub = dest.ub
+    if not src.is_continuous():
+        dest.domain = src.domain
+    if src.lb is not None:
+        if starting_lb is None:
+            dest.setlb(src.lb)
+        else:
+            dest.setlb(max(starting_lb, src.lb))
+    if src.ub is not None:
+        if starting_ub is None:
+            dest.setub(src.ub)
+        else:
+            dest.setub(min(starting_ub, src.ub))
+
+def tighten_var_domain(comp, new_var, index_set=None):
+    if index_set is None:
+        if comp.is_indexed():
+            index_set = comp.index_set()
+        else:
+            index_set = UnindexedComponent_set
+
     if comp.is_indexed():
         for i in index_set:
             try:
                 # set bounds for every member in case they differ
-                new_var[i].domain = comp[i].domain
-                new_var[i].setlb(comp[i].lb)
-                new_var[i].setub(comp[i].ub)
+                _tighten(comp[i], new_var[i])
             except AttributeError:
                 break
+    else:
+        try:
+            # set bounds for every member in case they differ
+            _tighten(comp, new_var)
+        except AttributeError:
+            pass
 
     return new_var
+
+def replicate_var(comp, name, block, index_set=None):
+    """
+    Create a new variable that will have the same indexing set, domain,
+    and bounds as the provided component, and add it to the given block.
+    Optionally pass an index set to use that to build the variable, but
+    this set must be symmetric to comp's index set.
+    """
+    new_var = create_var(comp, name, block, index_set)
+    tighten_var_domain(comp, new_var, index_set)
+    return new_var


Test patch:
diff --git a/pyomo/network/tests/test_arc.py b/pyomo/network/tests/test_arc.py
--- a/pyomo/network/tests/test_arc.py
+++ b/pyomo/network/tests/test_arc.py
@@ -984,6 +984,81 @@ def test_inactive(self):
     1 Declarations: v_equality
 """)
 
+    def test_extensive_no_splitfrac_single_var(self):
+        m = ConcreteModel()
+        m.x = Var()
+        m.y = Var()
+        m.z = Var()
+        m.p1 = Port(initialize={'v': (m.x, Port.Extensive, {'include_splitfrac':False})})
+        m.p2 = Port(initialize={'v': (m.y, Port.Extensive, {'include_splitfrac':False})})
+        m.p3 = Port(initialize={'v': (m.z, Port.Extensive, {'include_splitfrac':False})})
+        m.a1 = Arc(source=m.p1, destination=m.p2)
+        m.a2 = Arc(source=m.p1, destination=m.p3)
+
+        TransformationFactory('network.expand_arcs').apply_to(m)
+
+        os = StringIO()
+        m.pprint(ostream=os)
+        self.assertEqual(os.getvalue(),
+"""3 Var Declarations
+    x : Size=1, Index=None
+        Key  : Lower : Value : Upper : Fixed : Stale : Domain
+        None :  None :  None :  None : False :  True :  Reals
+    y : Size=1, Index=None
+        Key  : Lower : Value : Upper : Fixed : Stale : Domain
+        None :  None :  None :  None : False :  True :  Reals
+    z : Size=1, Index=None
+        Key  : Lower : Value : Upper : Fixed : Stale : Domain
+        None :  None :  None :  None : False :  True :  Reals
+
+3 Constraint Declarations
+    p1_v_outsum : Size=1, Index=None, Active=True
+        Key  : Lower : Body                              : Upper : Active
+        None :   0.0 : a1_expanded.v + a2_expanded.v - x :   0.0 :   True
+    p2_v_insum : Size=1, Index=None, Active=True
+        Key  : Lower : Body              : Upper : Active
+        None :   0.0 : a1_expanded.v - y :   0.0 :   True
+    p3_v_insum : Size=1, Index=None, Active=True
+        Key  : Lower : Body              : Upper : Active
+        None :   0.0 : a2_expanded.v - z :   0.0 :   True
+
+2 Block Declarations
+    a1_expanded : Size=1, Index=None, Active=True
+        1 Var Declarations
+            v : Size=1, Index=None
+                Key  : Lower : Value : Upper : Fixed : Stale : Domain
+                None :  None :  None :  None : False :  True :  Reals
+
+        1 Declarations: v
+    a2_expanded : Size=1, Index=None, Active=True
+        1 Var Declarations
+            v : Size=1, Index=None
+                Key  : Lower : Value : Upper : Fixed : Stale : Domain
+                None :  None :  None :  None : False :  True :  Reals
+
+        1 Declarations: v
+
+2 Arc Declarations
+    a1 : Size=1, Index=None, Active=False
+        Key  : Ports    : Directed : Active
+        None : (p1, p2) :     True :  False
+    a2 : Size=1, Index=None, Active=False
+        Key  : Ports    : Directed : Active
+        None : (p1, p3) :     True :  False
+
+3 Port Declarations
+    p1 : Size=1, Index=None
+        Key  : Name : Size : Variable
+        None :    v :    1 :        x
+    p2 : Size=1, Index=None
+        Key  : Name : Size : Variable
+        None :    v :    1 :        y
+    p3 : Size=1, Index=None
+        Key  : Name : Size : Variable
+        None :    v :    1 :        z
+
+13 Declarations: x y z p1 p2 p3 a1 a2 a1_expanded a2_expanded p1_v_outsum p2_v_insum p3_v_insum
+""")
 
     def test_extensive_single_var(self):
         m = ConcreteModel()
@@ -1061,6 +1136,132 @@ def test_extensive_single_var(self):
 13 Declarations: x y z p1 p2 p3 a1 a2 a1_expanded a2_expanded p1_v_outsum p2_v_insum p3_v_insum
 """)
 
+    def test_extensive_no_splitfrac_expansion(self):
+        m = ConcreteModel()
+        m.time = Set(initialize=[1, 2, 3])
+
+        m.source = Block()
+        m.load1 = Block()
+        m.load2 = Block()
+
+        def source_block(b):
+            b.p_out = Var(b.model().time)
+            b.outlet = Port(initialize={'p': (b.p_out, Port.Extensive, {'include_splitfrac':False})})
+
+        def load_block(b):
+            b.p_in = Var(b.model().time)
+            b.inlet = Port(initialize={'p': (b.p_in, Port.Extensive, {'include_splitfrac':False})})
+
+        source_block(m.source)
+        load_block(m.load1)
+        load_block(m.load2)
+
+        m.cs1 = Arc(source=m.source.outlet, destination=m.load1.inlet)
+        m.cs2 = Arc(source=m.source.outlet, destination=m.load2.inlet)
+
+        TransformationFactory("network.expand_arcs").apply_to(m)
+
+        ref = """
+1 Set Declarations
+    time : Dim=0, Dimen=1, Size=3, Domain=None, Ordered=False, Bounds=(1, 3)
+        [1, 2, 3]
+
+5 Block Declarations
+    cs1_expanded : Size=1, Index=None, Active=True
+        1 Var Declarations
+            p : Size=3, Index=time
+                Key : Lower : Value : Upper : Fixed : Stale : Domain
+                  1 :  None :  None :  None : False :  True :  Reals
+                  2 :  None :  None :  None : False :  True :  Reals
+                  3 :  None :  None :  None : False :  True :  Reals
+
+        1 Declarations: p
+    cs2_expanded : Size=1, Index=None, Active=True
+        1 Var Declarations
+            p : Size=3, Index=time
+                Key : Lower : Value : Upper : Fixed : Stale : Domain
+                  1 :  None :  None :  None : False :  True :  Reals
+                  2 :  None :  None :  None : False :  True :  Reals
+                  3 :  None :  None :  None : False :  True :  Reals
+
+        1 Declarations: p
+    load1 : Size=1, Index=None, Active=True
+        1 Var Declarations
+            p_in : Size=3, Index=time
+                Key : Lower : Value : Upper : Fixed : Stale : Domain
+                  1 :  None :  None :  None : False :  True :  Reals
+                  2 :  None :  None :  None : False :  True :  Reals
+                  3 :  None :  None :  None : False :  True :  Reals
+
+        1 Constraint Declarations
+            inlet_p_insum : Size=3, Index=time, Active=True
+                Key : Lower : Body                              : Upper : Active
+                  1 :   0.0 : cs1_expanded.p[1] - load1.p_in[1] :   0.0 :   True
+                  2 :   0.0 : cs1_expanded.p[2] - load1.p_in[2] :   0.0 :   True
+                  3 :   0.0 : cs1_expanded.p[3] - load1.p_in[3] :   0.0 :   True
+
+        1 Port Declarations
+            inlet : Size=1, Index=None
+                Key  : Name : Size : Variable
+                None :    p :    3 : load1.p_in
+
+        3 Declarations: p_in inlet inlet_p_insum
+    load2 : Size=1, Index=None, Active=True
+        1 Var Declarations
+            p_in : Size=3, Index=time
+                Key : Lower : Value : Upper : Fixed : Stale : Domain
+                  1 :  None :  None :  None : False :  True :  Reals
+                  2 :  None :  None :  None : False :  True :  Reals
+                  3 :  None :  None :  None : False :  True :  Reals
+
+        1 Constraint Declarations
+            inlet_p_insum : Size=3, Index=time, Active=True
+                Key : Lower : Body                              : Upper : Active
+                  1 :   0.0 : cs2_expanded.p[1] - load2.p_in[1] :   0.0 :   True
+                  2 :   0.0 : cs2_expanded.p[2] - load2.p_in[2] :   0.0 :   True
+                  3 :   0.0 : cs2_expanded.p[3] - load2.p_in[3] :   0.0 :   True
+
+        1 Port Declarations
+            inlet : Size=1, Index=None
+                Key  : Name : Size : Variable
+                None :    p :    3 : load2.p_in
+
+        3 Declarations: p_in inlet inlet_p_insum
+    source : Size=1, Index=None, Active=True
+        1 Var Declarations
+            p_out : Size=3, Index=time
+                Key : Lower : Value : Upper : Fixed : Stale : Domain
+                  1 :  None :  None :  None : False :  True :  Reals
+                  2 :  None :  None :  None : False :  True :  Reals
+                  3 :  None :  None :  None : False :  True :  Reals
+
+        1 Constraint Declarations
+            outlet_p_outsum : Size=3, Index=time, Active=True
+                Key : Lower : Body                                                    : Upper : Active
+                  1 :   0.0 : cs1_expanded.p[1] + cs2_expanded.p[1] - source.p_out[1] :   0.0 :   True
+                  2 :   0.0 : cs1_expanded.p[2] + cs2_expanded.p[2] - source.p_out[2] :   0.0 :   True
+                  3 :   0.0 : cs1_expanded.p[3] + cs2_expanded.p[3] - source.p_out[3] :   0.0 :   True
+
+        1 Port Declarations
+            outlet : Size=1, Index=None
+                Key  : Name : Size : Variable
+                None :    p :    3 : source.p_out
+
+        3 Declarations: p_out outlet outlet_p_outsum
+
+2 Arc Declarations
+    cs1 : Size=1, Index=None, Active=False
+        Key  : Ports                        : Directed : Active
+        None : (source.outlet, load1.inlet) :     True :  False
+    cs2 : Size=1, Index=None, Active=False
+        Key  : Ports                        : Directed : Active
+        None : (source.outlet, load2.inlet) :     True :  False
+
+8 Declarations: time source load1 load2 cs1 cs2 cs1_expanded cs2_expanded
+"""
+        os = StringIO()
+        m.pprint(ostream=os)
+        self.assertEqual(os.getvalue().strip(), ref.strip())
 
     def test_extensive_expansion(self):
         m = ConcreteModel()
@@ -1332,9 +1533,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :  None :  None :  None : False :  True :  Reals
+                  b :  None :  None :  None : False :  True :  Reals
+                  c :  None :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1360,9 +1561,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :  None :  None :  None : False :  True :  Reals
+                  b :  None :  None :  None : False :  True :  Reals
+                  c :  None :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1388,9 +1589,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :  None :  None :  None : False :  True :  Reals
+                  b :  None :  None :  None : False :  True :  Reals
+                  c :  None :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1416,9 +1617,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :     0 :  None :  None : False :  True :  Reals
+                  b :     0 :  None :  None : False :  True :  Reals
+                  c :     0 :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1459,9 +1660,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :  None :  None :  None : False :  True :  Reals
+                  b :  None :  None :  None : False :  True :  Reals
+                  c :  None :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1487,9 +1688,9 @@ def test_extensive_expansion(self):
         2 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :     0 :  None :  None : False :  True :  Reals
+                  b :     0 :  None :  None : False :  True :  Reals
+                  c :     0 :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1504,9 +1705,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :  None :  None :  None : False :  True :  Reals
+                  b :  None :  None :  None : False :  True :  Reals
+                  c :  None :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1532,9 +1733,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :  None :  None :  None : False :  True :  Reals
+                  b :  None :  None :  None : False :  True :  Reals
+                  c :  None :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals
@@ -1560,9 +1761,9 @@ def test_extensive_expansion(self):
         3 Var Declarations
             flow : Size=3, Index=comp
                 Key : Lower : Value : Upper : Fixed : Stale : Domain
-                  a :     0 :  None :  None : False :  True : NonNegativeReals
-                  b :     0 :  None :  None : False :  True : NonNegativeReals
-                  c :     0 :  None :  None : False :  True : NonNegativeReals
+                  a :  None :  None :  None : False :  True :  Reals
+                  b :  None :  None :  None : False :  True :  Reals
+                  c :  None :  None :  None : False :  True :  Reals
             mass : Size=1, Index=None
                 Key  : Lower : Value : Upper : Fixed : Stale : Domain
                 None :  None :  None :  None : False :  True :  Reals