# ____________________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2026 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 rights in this
# software. This software is distributed under the 3-clause BSD License.
# ____________________________________________________________________________________
import operator
from math import prod as _prod
import pyomo.core.expr as EXPR
from pyomo.common import DeveloperError
from pyomo.common.collections import ComponentMap
from pyomo.common.dependencies import attempt_import
from pyomo.common.errors import NondifferentiableError
from pyomo.core.expr.numvalue import value, native_types
#
# Sympy takes a significant time to load; defer importing it unless
# someone actually needs the interface.
#
_operatorMap = {}
_pyomo_operator_map = {}
_functionMap = {}
def _nondifferentiable(x):
if type(x[1]) is tuple:
# sympy >= 1.3 returns tuples (var, order)
wrt = x[1][0]
else:
# early versions of sympy returned the bare var
wrt = x[1]
raise NondifferentiableError(
"The sub-expression '%s' is not differentiable with respect to %s" % (x[0], wrt)
)
def _external_fcn(*x):
raise TypeError(
"Expressions containing external functions are not convertible to "
f"sympy expressions (found 'f{x}')"
)
def _configure_sympy(sympy, available):
if not available:
return
_operatorMap.update(
{
sympy.Add: sum,
sympy.Mul: _prod,
sympy.Pow: lambda x: operator.pow(*x),
sympy.exp: lambda x: EXPR.exp(*x),
sympy.log: lambda x: EXPR.log(*x),
sympy.sin: lambda x: EXPR.sin(*x),
sympy.asin: lambda x: EXPR.asin(*x),
sympy.sinh: lambda x: EXPR.sinh(*x),
sympy.asinh: lambda x: EXPR.asinh(*x),
sympy.cos: lambda x: EXPR.cos(*x),
sympy.acos: lambda x: EXPR.acos(*x),
sympy.cosh: lambda x: EXPR.cosh(*x),
sympy.acosh: lambda x: EXPR.acosh(*x),
sympy.tan: lambda x: EXPR.tan(*x),
sympy.atan: lambda x: EXPR.atan(*x),
sympy.tanh: lambda x: EXPR.tanh(*x),
sympy.atanh: lambda x: EXPR.atanh(*x),
sympy.ceiling: lambda x: EXPR.ceil(*x),
sympy.floor: lambda x: EXPR.floor(*x),
sympy.sqrt: lambda x: EXPR.sqrt(*x),
sympy.Abs: lambda x: abs(*x),
sympy.Derivative: _nondifferentiable,
sympy.Tuple: lambda x: x,
sympy.Or: lambda x: EXPR.lor(*x),
sympy.And: lambda x: EXPR.land(*x),
sympy.Implies: lambda x: EXPR.implies(*x),
sympy.Equivalent: lambda x: EXPR.equivalents(*x),
sympy.Not: lambda x: EXPR.lnot(*x),
sympy.LessThan: lambda x: operator.le(*x),
sympy.StrictLessThan: lambda x: operator.lt(*x),
sympy.GreaterThan: lambda x: operator.ge(*x),
sympy.StrictGreaterThan: lambda x: operator.gt(*x),
sympy.Equality: lambda x: operator.eq(*x),
}
)
_pyomo_operator_map.update(
{
EXPR.SumExpression: sympy.Add,
EXPR.LinearExpression: sympy.Add,
EXPR.ProductExpression: sympy.Mul,
EXPR.MonomialTermExpression: sympy.Mul,
EXPR.ExternalFunctionExpression: _external_fcn,
EXPR.AndExpression: sympy.And,
EXPR.OrExpression: sympy.Or,
EXPR.ImplicationExpression: sympy.Implies,
EXPR.EquivalenceExpression: sympy.Equivalent,
EXPR.XorExpression: sympy.Xor,
EXPR.NotExpression: sympy.Not,
}
)
_functionMap.update(
{
'exp': sympy.exp,
'log': sympy.log,
'log10': lambda x: sympy.log(x) / sympy.log(10),
'sin': sympy.sin,
'asin': sympy.asin,
'sinh': sympy.sinh,
'asinh': sympy.asinh,
'cos': sympy.cos,
'acos': sympy.acos,
'cosh': sympy.cosh,
'acosh': sympy.acosh,
'tan': sympy.tan,
'atan': sympy.atan,
'tanh': sympy.tanh,
'atanh': sympy.atanh,
'ceil': sympy.ceiling,
'floor': sympy.floor,
'sqrt': sympy.sqrt,
}
)
sympy, sympy_available = attempt_import('sympy', callback=_configure_sympy)
[docs]
class PyomoSympyBimap:
[docs]
def __init__(self):
self.pyomo2sympy = ComponentMap()
self.sympy2pyomo = {}
self.i = 0
def getPyomoSymbol(self, sympy_object, default=None):
return self.sympy2pyomo.get(sympy_object, default)
def getSympySymbol(self, pyomo_object):
if pyomo_object in self.pyomo2sympy:
return self.pyomo2sympy[pyomo_object]
# Pyomo currently ONLY supports Real variables (not complex
# variables). If that ever changes, then we will need to
# revisit hard-coding the symbol type here
sympy_obj = sympy.Symbol("x%d" % self.i, real=True)
self.i += 1
self.pyomo2sympy[pyomo_object] = sympy_obj
self.sympy2pyomo[sympy_obj] = pyomo_object
return sympy_obj
def sympyVars(self):
return self.sympy2pyomo.keys()
# =====================================================
# sympyify_expression
# =====================================================
[docs]
class Pyomo2SympyVisitor(EXPR.StreamBasedExpressionVisitor):
[docs]
def __init__(self, object_map, keep_mutable_parameters=False):
sympy.Add # this ensures _configure_sympy gets run
super(Pyomo2SympyVisitor, self).__init__()
self.object_map = object_map
self.keep_mutable_parameters = keep_mutable_parameters
def initializeWalker(self, expr):
return self.beforeChild(None, expr, None)
def exitNode(self, node, values):
if node.__class__ is EXPR.UnaryFunctionExpression:
return _functionMap[node._name](values[0])
_op = _pyomo_operator_map.get(node.__class__, None)
if _op is None:
return node._apply_operation(values)
else:
return _op(*tuple(values))
def beforeChild(self, node, child, child_idx):
#
# Don't replace native or sympy types
#
if type(child) in native_types:
return False, child
#
# Replace pyomo variables with sympy variables
#
if child.is_potentially_variable():
#
# We will descend into all expressions...
#
if child.is_expression_type():
return True, None
else:
return False, self.object_map.getSympySymbol(child)
#
# Everything else is a constant...
#
if self.keep_mutable_parameters and child.is_parameter_type() and child.mutable:
return False, self.object_map.getSympySymbol(child)
return False, value(child)
[docs]
class Sympy2PyomoVisitor(EXPR.StreamBasedExpressionVisitor):
[docs]
def __init__(self, object_map):
sympy.Add # this ensures _configure_sympy gets run
super(Sympy2PyomoVisitor, self).__init__()
self.object_map = object_map
def initializeWalker(self, expr):
return self.beforeChild(None, expr, None)
def enterNode(self, node):
return (node.args, [])
[docs]
def exitNode(self, node, values):
"""Visit nodes that have been expanded"""
_op = _operatorMap.get(node.func, None)
if _op is None:
raise DeveloperError(
f"sympy expression type {node.func} not found in the operator map"
)
return _op(tuple(values))
def beforeChild(self, node, child, child_idx):
if not child.args:
item = self.object_map.getPyomoSymbol(child, None)
if item is None:
item = float(child.evalf())
return False, item
return True, None
[docs]
def sympyify_expression(expr, keep_mutable_parameters=False):
"""Convert a Pyomo expression to a Sympy expression"""
#
# Create the visitor and call it.
#
object_map = PyomoSympyBimap()
visitor = Pyomo2SympyVisitor(
object_map, keep_mutable_parameters=keep_mutable_parameters
)
return object_map, visitor.walk_expression(expr)
[docs]
def sympy2pyomo_expression(expr, object_map):
visitor = Sympy2PyomoVisitor(object_map)
return visitor.walk_expression(expr)