Managing Expressions
Creating a String Representation of an Expression
There are several ways that string representations can be created
from an expression, but the expression_to_string
function provides
the most flexible mechanism for generating a string representation.
The options to this function control distinct aspects of the string
representation.
Algebraic vs. Nested Functional Form
The default string representation is an algebraic form, which closely
mimics the Python operations used to construct an expression. The
verbose
flag can be set to True
to generate a
string representation that is a nested functional form. For example:
import pyomo.core.expr as EXPR
M = ConcreteModel()
M.x = Var()
e = sin(M.x) + 2 * M.x
# sin(x) + 2*x
print(EXPR.expression_to_string(e))
# sum(sin(x), prod(2, x))
print(EXPR.expression_to_string(e, verbose=True))
Labeler and Symbol Map
The string representation used for variables in expression can be
customized to define different label formats. If the labeler
option is specified, then this function (or class functor) is used to
generate a string label used to represent the variable. Pyomo defines a
variety of labelers in the pyomo.core.base.label module. For example,
the NumericLabeler
defines a functor that can be used to
sequentially generate simple labels with a prefix followed by the
variable count:
import pyomo.core.expr as EXPR
M = ConcreteModel()
M.x = Var()
M.y = Var()
e = sin(M.x) + 2 * M.y
# sin(x1) + 2*x2
print(EXPR.expression_to_string(e, labeler=NumericLabeler('x')))
The smap
option is used to specify a symbol map object
(SymbolMap
), which
caches the variable label data. This option is normally specified
in contexts where the string representations for many expressions
are being generated. In that context, a symbol map ensures that
variables in different expressions have a consistent label in their
associated string representations.
Other Ways to Generate String Representations
There are two other standard ways to generate string representations:
Call the
__str__()
magic method (e.g. using the Pythonstr()
function. This callsexpression_to_string
, using the default values for all arguments.Call the
to_string()
method on theExpressionBase
class. This callsexpression_to_string
and accepts the same arguments.
Evaluating Expressions
Expressions can be evaluated when all variables and parameters in
the expression have a value. The value
function can be used to walk the expression tree and compute the
value of an expression. For example:
M = ConcreteModel()
M.x = Var()
M.x.value = math.pi / 2.0
val = value(M.x)
assert isclose(val, math.pi / 2.0)
Additionally, expressions define the __call__()
method, so the
following is another way to compute the value of an expression:
val = M.x()
assert isclose(val, math.pi / 2.0)
If a parameter or variable is undefined, then the value
function and __call__()
method will
raise an exception. This exception can be suppressed using the
exception
option. For example:
M = ConcreteModel()
M.x = Var()
val = value(M.x, exception=False)
assert val is None
This option is useful in contexts where adding a try block is inconvenient in your modeling script.
Note
Both the value
function and
__call__()
method call the evaluate_expression
function. In
practice, this function will be slightly faster, but the
difference is only meaningful when expressions are evaluated
many times.
Identifying Components and Variables
Expression transformations sometimes need to find all nodes in an
expression tree that are of a given type. Pyomo contains two utility
functions that support this functionality. First, the
identify_components
function is a generator function that walks the expression tree and yields all
nodes whose type is in a specified set of node types. For example:
import pyomo.core.expr as EXPR
M = ConcreteModel()
M.x = Var()
M.p = Param(mutable=True)
e = M.p + M.x
s = set([type(M.p)])
assert list(EXPR.identify_components(e, s)) == [M.p]
The identify_variables
function is a generator function that yields all nodes that are
variables. Pyomo uses several different classes to represent variables,
but this set of variable types does not need to be specified by the user.
However, the include_fixed
flag can be specified to omit fixed
variables. For example:
import pyomo.core.expr as EXPR
M = ConcreteModel()
M.x = Var()
M.y = Var()
e = M.x + M.y
M.y.value = 1
M.y.fixed = True
assert set(id(v) for v in EXPR.identify_variables(e)) == set([id(M.x), id(M.y)])
assert set(id(v) for v in EXPR.identify_variables(e, include_fixed=False)) == set(
[id(M.x)]
)
Walking an Expression Tree with a Visitor Class
Many of the utility functions defined above are implemented by walking an expression tree and performing an operation at nodes in the tree. For example, evaluating an expression is performed using a post-order depth-first search process where the value of a node is computed using the values of its children.
Walking an expression tree can be tricky, and the code requires intimate knowledge of the design of the expression system. Pyomo includes several classes that define visitor patterns for walking expression tree:
StreamBasedExpressionVisitor
The most general and extensible visitor class. This visitor implements an event-based approach for walking the tree inspired by the
expat
library for processing XML files. The visitor has seven event callbacks that users can hook into, providing very fine-grained control over the expression walker.ExpressionValueVisitor
When the
visitor()
method is called on each node in the tree, the values of its children have been computed. The value of the node is returned fromvisitor()
.ExpressionReplacementVisitor
When the
visitor()
method is called on each node in the tree, it may clone or otherwise replace the node using objects for its children (which themselves may be clones or replacements from the original child objects). The new node object is returned fromvisitor()
.
These classes define a variety of suitable tree search methods:
StreamBasedExpressionVisitor
walk_expression
: depth-first traversal of the expression tree.
ExpressionReplacementVisitor
walk_expression
: depth-first traversal of the expression tree.
ExpressionValueVisitor
dfs_postorder_stack
: postorder depth-first search using a nonrecursive stack
To implement a visitor object, a user needs to provide specializations
for specific events. For legacy visitors based on the PyUtilib visitor
pattern (e.g., ExpressionValueVisitor
), one must create a
subclass and override at least one of the following:
visit()
Defines the operation that is performed when a node is visited. In the
ExpressionValueVisitor
andExpressionReplacementVisitor
visitor classes, this method returns a value that is used by its parent node.visiting_potential_leaf()
Checks if the search should terminate with this node. If no, then this method returns the tuple
(False, None)
. If yes, then this method returns(False, value)
, where value is computed by this method.finalize()
This method defines the final value that is returned from the visitor. This is not normally redefined.
For modern visitors based on the StreamBasedExpressionVisitor
, one can either define a
subclass, pass the callbacks to an instance of the base class, or assign
the callbacks as attributes on an instance of the base class. The
StreamBasedExpressionVisitor
provides seven
callbacks, which are documented in the class documentation.
Detailed documentation of the APIs for these methods is provided with the class documentation for these visitors.
StreamBasedExpressionVisitor Example
In this example, we describe an visitor class that counts the number of nodes in an expression (including leaf nodes). Consider the following class:
import pyomo.core.expr as EXPR
class SizeofVisitor(EXPR.StreamBasedExpressionVisitor):
def initializeWalker(self, expr):
self.counter = 0
return True, expr
def exitNode(self, node, data):
self.counter += 1
def finalizeResult(self, result):
return self.counter
The initializeWalker()
method creates a counter, and the
exitNode()
method increments this counter for every node that is
visited. The finalizeResult()
method returns the value of this
counter after the tree has been walked. The following function
illustrates this use of this visitor class:
def sizeof_expression(expr):
#
# Create the visitor object
#
visitor = SizeofVisitor()
#
# Compute the value using the :func:`walk_expression` search method.
#
return visitor.walk_expression(expr)
ExpressionValueVisitor Example
In this example, we describe an visitor class that clones the expression tree (including leaf nodes). Consider the following class:
import pyomo.core.expr as EXPR
class CloneVisitor(EXPR.ExpressionValueVisitor):
def __init__(self):
self.memo = {'__block_scope__': {id(None): False}}
def visit(self, node, values):
#
# Clone the interior node
#
return node.create_node_with_local_data(values)
def visiting_potential_leaf(self, node):
#
# Clone leaf nodes in the expression tree
#
if node.__class__ in native_numeric_types or not node.is_expression_type():
return True, copy.deepcopy(node, self.memo)
return False, None
The visit()
method creates a new expression node with children
specified by values
. The visiting_potential_leaf()
method performs a deepcopy()
on leaf nodes, which are native
Python types or non-expression objects.
def clone_expression(expr):
#
# Create the visitor object
#
visitor = CloneVisitor()
#
# Clone the expression using the :func:`dfs_postorder_stack`
# search method.
#
return visitor.dfs_postorder_stack(expr)
ExpressionReplacementVisitor Example
In this example, we describe an visitor class that replaces variables with scaled variables, using a mutable parameter that can be modified later. the following class:
import pyomo.core.expr as EXPR
class ScalingVisitor(EXPR.ExpressionReplacementVisitor):
def __init__(self, scale):
super(ScalingVisitor, self).__init__()
self.scale = scale
def beforeChild(self, node, child, child_idx):
#
# Native numeric types are terminal nodes; this also catches all
# nodes that do not conform to the ExpressionBase API (i.e.,
# define is_variable_type)
#
if child.__class__ in native_numeric_types:
return False, child
#
# Replace leaf variables with scaled variables
#
if child.is_variable_type():
return False, self.scale[id(child)] * child
#
# Everything else can be processed normally
#
return True, None
No other method need to be defined. The
beforeChild()
method identifies variable nodes
and returns a product expression that contains a mutable parameter.
def scale_expression(expr, scale):
#
# Create the visitor object
#
visitor = ScalingVisitor(scale)
#
# Scale the expression using the :func:`dfs_postorder_stack`
# search method.
#
return visitor.walk_expression(expr)
The scale_expression()
function is called with an expression and
a dictionary, scale
, that maps variable ID to model parameter. For example:
M = ConcreteModel()
M.x = Var(range(5))
M.p = Param(range(5), mutable=True)
scale = {}
for i in M.x:
scale[id(M.x[i])] = M.p[i]
e = quicksum(M.x[i] for i in M.x)
f = scale_expression(e, scale)
# p[0]*x[0] + p[1]*x[1] + p[2]*x[2] + p[3]*x[3] + p[4]*x[4]
print(f)