Skip to content

Complex numbers in expression trees #879

Open
@jsiirola

Description

@jsiirola

Working on a PR #872 highlighted a rather nefarious situation in the Pyomo expression system. Python is rather inconsistent in how it treats complex numbers. For example, in Python 2.x:

>>> import math
>>> import cmath
>>> cmath.sqrt(-1)
1j
>>> math.sqrt(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> (-1)**(.5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: negative number cannot be raised to a fractional power

but in Python 3, we see:

>>> import math
>>> import cmath
>>> cmath.sqrt(-1)
1j
>>> math.sqrt(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> (-1)**(.5)
(6.123233995736766e-17+1j)

This is problematic in a number of ways. First, in some versions of Python, the pow() operator will raise an exception and in others it will happily return a complex number:

>>> m = ConcreteModel()
>>> m.p = Param(initialize=-1, mutable=True)
>>> m.e = Expression(expr = m.p**0.5)

# Python2.x:
>>> value(m.e)
ERROR: evaluating object as numeric value: e
        (object: <class 'pyomo.core.base.expression.SimpleExpression'>)
    negative number cannot be raised to a fractional power

# Python 3.x:
>>> value(m.e)
(6.123233995736766e-17+1j)

This is further confounded by our use of the math library to evaluate unary operators. For ALL versions of Python, the math library operations will raise exceptions when they encounter complex numbers, so the following will raise exceptions in all Python versions; however, the exception that is raised will be different:

>>> m.f = Expression(expr=exp(m.e))

# Python 2:
>>> value(m.f)
ERROR: evaluating object as numeric value: f
        (object: <class 'pyomo.core.base.expression.SimpleExpression'>)
    negative number cannot be raised to a fractional power
    [...]
ValueError: negative number cannot be raised to a fractional power

# Python 3:
>>> value(m.f)
ERROR: evaluating object as numeric value: f
        (object: <class 'pyomo.core.base.expression.SimpleExpression'>)
    can't convert complex to float
    [...]
TypeError: can't convert complex to float

Finally, in all versions of Python, complex numbers are not comparable, and and relative comparison (<,<=) will raise a TypeError.

So, what should we do? I can see several options:

  1. Disallow all complex numbers in Pyomo. We can trap for complex numbers at key locations (i.e., in pow(), and Var/Param set_value()) and raise an exception immediately. For consistency with Python2, we should probably raise a ValueError.

    • Pro: No (current) Pyomo solver supports complex variables, so we shouldn't incur the pain / technical debt of them either.
    • Pro: Minimal change to the current codebase.
    • Con: this will explicitly preclude support for complex numbers in Pyomo (and any future solver that can support complex numbers)
  2. We could allow (more) use of complex numbers in the expression system. All of our unary operations have equivalents in the cmath library that we could (quietly) substitute in, except for floor and ceil. Evaluating expressions that had invalid operators in them (inequality, floor, ceil) would continue to raise a TypeError.

    • Pro: this would make Pyomo more flexible, and would make our expression system behave more like Python's.
    • Pro: this would allow for future solvers that could explicitly deal with complex numbers.
    • Pro: this is a simple change (just changing where we import the unary functions from); BUT:
    • Con: This could open up a testing nightmare (although I would argue that it already exists and we have just not triggered it before now). The bulk of our codebase assumes that all variables / parameters are real values. We could be opening ourselves to another lifetime of tracking down exceptions where complex numbers appeared where we had never thought they might.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions