Expression Entanglement and Mutability
Pyomo fundamentally relies on the use of magic methods in Python
to generate expression trees, which means that Pyomo has very limited
control for how expressions are managed in Python. For example:
Python variables can point to the same expression tree
M = pyo.ConcreteModel()
M.v = pyo.Var()
e = f = 2 * M.v
This is illustrated as follows:
A variable can point to a sub-tree that another variable points to
M = pyo.ConcreteModel()
M.v = pyo.Var()
e = 2 * M.v
f = e + 3
This is illustrated as follows:
Two expression trees can point to the same sub-tree
M = pyo.ConcreteModel()
M.v = pyo.Var()
e = 2 * M.v
f = e + 3
g = e + 4
This is illustrated as follows:
In each of these examples, it is almost impossible for a Pyomo user
or developer to detect whether expressions are being shared. In
CPython, the reference counting logic can support this to a limited
degree. But no equivalent mechanisms are available in PyPy and
other Python implementations.
Entangled Sub-Expressions
We say that expressions are entangled if they share one or more
sub-expressions. The first example above does not represent
entanglement, but rather the fact that multiple Python variables
can point to the same expression tree. In the second and third
examples, the expressions are entangled because the subtree represented
by e is shared. However, if a leave node like M.v is shared
between expressions, we do not consider those expressions entangled.
Expression entanglement is problematic because shared expressions complicate
the expected behavior when sub-expressions are changed. Consider the following example:
M = pyo.ConcreteModel()
M.v = pyo.Var()
M.w = pyo.Var()
e = 2 * M.v
f = e + 3
e += M.w
What is the value of e after M.w is added to it? What is the
value of f? The answers to these questions are not immediately
obvious, and the fact that Coopr3 uses mutable expression objects
makes them even less clear. However, Pyomo5 and Coopr3 enforce
the following semantics:
A change to an expression e that is a sub-expression of f
does not change the expression tree for f.
This property ensures a change to an expression does not create side effects that change the
values of other, previously defined expressions.
For instance, the previous example results in the following (in Pyomo5):
With Pyomo5 expressions, each sub-expression is immutable. Thus,
the summation operation generates a new expression e without
changing existing expression objects referenced in the expression
tree for f. By contrast, Coopr3 imposes the same property by
cloning the expression e before added M.w, resulting in the following:
This example also illustrates that leaves may be shared between expressions.
Mutable Expression Components
There is one important exception to the entanglement property
described above. The Expression component is treated as a
mutable expression when shared between expressions. For example:
M = pyo.ConcreteModel()
M.v = pyo.Var()
M.w = pyo.Var()
M.e = pyo.Expression(expr=2 * M.v)
f = M.e + 3
M.e += M.w
Here, the expression M.e is a so-called named expression that
the user has declared. Named expressions are explicitly intended
for re-use within models, and they provide a convenient mechanism
for changing sub-expressions in complex applications. In this example, the
expression tree is as follows before M.w is added:
And the expression tree is as follows after M.w is added.
When considering named expressions, Pyomo5 and Coopr3 enforce
the following semantics:
A change to a named expression e that is a sub-expression of
f changes the expression tree for f, because f continues
to point to e after it is changed.