Skip to content

Commit

Permalink
Update platform support and require symengine
Browse files Browse the repository at this point in the history
This commit updates our platform support matrix to reflect upcoming
changes. The first is that in Rust 1.74 the Rust programming language is
raising their minimum support macOS version to 10.12, so Qiskit is
raising it's supported version of macOS to match this. The second change
is making symengine a hard requirement. We previously had symengine as a
requirement only on platforms that had precompiled packages available.
But, the percentage of our user base that runs qiskit on those platforms
is very small, and maintaining dual support for symengine and sympy adds
a lot of complexity around managing the dependencies. This commit
promotes symengine to a hard requirement for all users regardless of
platform. As a result Linux i686 and 32 bit Windows for Python < 3.10
has been downgraded to tier 3 support as you'll need a C++ to install
Qiskit on that platform now (regardless of Python version).
  • Loading branch information
mtreinish committed Sep 26, 2023
1 parent 6e80e89 commit 9a1d01b
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 220 deletions.
10 changes: 3 additions & 7 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Tier 1 platforms are currently:
* Linux x86_64 (distributions compatible with the
`manylinux 2014 <https://www.python.org/dev/peps/pep-0599/>`__
packaging specification).
* macOS x86_64 (10.9 or newer)
* macOS x86_64 (10.12 or newer)
* Windows 64 bit

Tier 2
Expand All @@ -211,10 +211,6 @@ functioning Python environment.

Tier 2 platforms are currently:

* Linux i686 (distributions compatible with the
`manylinux 2014 <https://www.python.org/dev/peps/pep-0599/>`__ packaging
specification) for Python < 3.10
* Windows 32 bit for Python < 3.10
* Linux aarch64 (distributions compatible with the
`manylinux 2014 <https://www.python.org/dev/peps/pep-0599/>`__ packaging
specification)
Expand All @@ -240,8 +236,8 @@ Tier 3 platforms are currently:
* macOS arm64 (10.15 or newer)
* Linux i686 (distributions compatible with the
`manylinux 2014 <https://www.python.org/dev/peps/pep-0599/>`__ packaging
specification) for Python >= 3.10
* Windows 32 bit for Python >= 3.10
specification)
* Windows 32 bit

Ready to get going?...
======================
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ target-version = ['py38', 'py39', 'py310', 'py311']
manylinux-x86_64-image = "manylinux2014"
manylinux-i686-image = "manylinux2014"
skip = "pp* cp36-* cp37-* *musllinux*"
test-skip = "cp310-win32 cp310-manylinux_i686 cp311-win32 cp311-manylinux_i686"
test-skip = "*win32 *linux_i686"
test-command = "python {project}/examples/python/stochastic_swap.py"
# We need to use pre-built versions of Numpy and Scipy in the tests; they have a
# tendency to crash if they're installed from source by `pip install`, and since
Expand All @@ -25,6 +25,7 @@ environment = 'PATH="$PATH:$HOME/.cargo/bin" CARGO_NET_GIT_FETCH_WITH_CLI="true"
repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel} && pipx run abi3audit --strict --report {wheel}"

[tool.cibuildwheel.macos]
environment = "MACOSX_DEPLOYMENT_TARGET=10.12"
repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && pipx run abi3audit --strict --report {wheel}"

[tool.cibuildwheel.windows]
Expand Down
27 changes: 5 additions & 22 deletions qiskit/circuit/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@

from uuid import uuid4

import symengine

from qiskit.circuit.exceptions import CircuitError
from qiskit.utils import optionals as _optionals

from .parameterexpression import ParameterExpression

Expand Down Expand Up @@ -80,14 +81,7 @@ def __init__(self, name: str):
be any unicode string, e.g. "ϕ".
"""
self._name = name
if not _optionals.HAS_SYMENGINE:
from sympy import Symbol

symbol = Symbol(name)
else:
import symengine

symbol = symengine.Symbol(name)
symbol = symengine.Symbol(name)
super().__init__(symbol_map={self: symbol}, expr=symbol)

def assign(self, parameter, value):
Expand All @@ -102,11 +96,7 @@ def assign(self, parameter, value):
return value
# This is the `super().bind` case, where we're required to return a `ParameterExpression`,
# so we need to lift the given value to a symbolic expression.
if _optionals.HAS_SYMENGINE:
from symengine import sympify
else:
from sympy import sympify
return ParameterExpression({}, sympify(value))
return ParameterExpression({}, symengine.sympify(value))

def subs(self, parameter_map: dict, allow_unknown_parameters: bool = False):
"""Substitute self with the corresponding parameter in ``parameter_map``."""
Expand Down Expand Up @@ -152,12 +142,5 @@ def __getstate__(self):

def __setstate__(self, state):
self._name = state["name"]
if not _optionals.HAS_SYMENGINE:
from sympy import Symbol

symbol = Symbol(self._name)
else:
import symengine

symbol = symengine.Symbol(self._name)
symbol = symengine.Symbol(self._name)
super().__init__(symbol_map={self: symbol}, expr=symbol)
160 changes: 31 additions & 129 deletions qiskit/circuit/parameterexpression.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
import operator

import numpy
import symengine

from qiskit.circuit.exceptions import CircuitError
from qiskit.utils import optionals as _optionals

# This type is redefined at the bottom to insert the full reference to "ParameterExpression", so it
# can safely be used by runtime type-checkers like Sphinx. Mypy does not need this because it
Expand Down Expand Up @@ -66,14 +66,9 @@ def _names(self) -> dict:

def conjugate(self) -> "ParameterExpression":
"""Return the conjugate."""
if _optionals.HAS_SYMENGINE:
import symengine

conjugated = ParameterExpression(
self._parameter_symbols, symengine.conjugate(self._symbol_expr)
)
else:
conjugated = ParameterExpression(self._parameter_symbols, self._symbol_expr.conjugate())
conjugated = ParameterExpression(
self._parameter_symbols, symengine.conjugate(self._symbol_expr)
)
return conjugated

def assign(self, parameter, value: ParameterValueType) -> "ParameterExpression":
Expand Down Expand Up @@ -183,15 +178,7 @@ def subs(
new_parameter_symbols = {
p: s for p, s in self._parameter_symbols.items() if p not in parameter_map
}

if _optionals.HAS_SYMENGINE:
import symengine

symbol_type = symengine.Symbol
else:
from sympy import Symbol

symbol_type = Symbol
symbol_type = symengine.Symbol

# If new_param is an expr, we'll need to construct a matching sympy expr
# but with our sympy symbols instead of theirs.
Expand Down Expand Up @@ -304,15 +291,7 @@ def gradient(self, param) -> Union["ParameterExpression", complex]:

# Compute the gradient of the parameter expression w.r.t. param
key = self._parameter_symbols[param]
if _optionals.HAS_SYMENGINE:
import symengine

expr_grad = symengine.Derivative(self._symbol_expr, key)
else:
# TODO enable nth derivative
from sympy import Derivative

expr_grad = Derivative(self._symbol_expr, key).doit()
expr_grad = symengine.Derivative(self._symbol_expr, key)

# generate the new dictionary of symbols
# this needs to be done since in the derivative some symbols might disappear (e.g.
Expand Down Expand Up @@ -365,102 +344,39 @@ def _call(self, ufunc):

def sin(self):
"""Sine of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.sin)
else:
from sympy import sin as _sin

return self._call(_sin)
return self._call(symengine.sin)

def cos(self):
"""Cosine of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.cos)
else:
from sympy import cos as _cos

return self._call(_cos)
return self._call(symengine.cos)

def tan(self):
"""Tangent of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.tan)
else:
from sympy import tan as _tan

return self._call(_tan)
return self._call(symengine.tan)

def arcsin(self):
"""Arcsin of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.asin)
else:
from sympy import asin as _asin

return self._call(_asin)
return self._call(symengine.asin)

def arccos(self):
"""Arccos of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.acos)
else:
from sympy import acos as _acos

return self._call(_acos)
return self._call(symengine.acos)

def arctan(self):
"""Arctan of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.atan)
else:
from sympy import atan as _atan

return self._call(_atan)
return self._call(symengine.atan)

def exp(self):
"""Exponential of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.exp)
else:
from sympy import exp as _exp

return self._call(_exp)
return self._call(symengine.exp)

def log(self):
"""Logarithm of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.log)
else:
from sympy import log as _log

return self._call(_log)
return self._call(symengine.log)

def sign(self):
"""Sign of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.sign)
else:
from sympy import sign as _sign

return self._call(_sign)
return self._call(symengine.sign)

def __repr__(self):
return f"{self.__class__.__name__}({str(self)})"
Expand Down Expand Up @@ -492,24 +408,21 @@ def __float__(self):
"ParameterExpression with unbound parameters ({}) "
"cannot be cast to a float.".format(self.parameters)
) from None
try:
# In symengine, if an expression was complex at any time, its type is likely to have
# stayed "complex" even when the imaginary part symbolically (i.e. exactly)
# cancelled out. Sympy tends to more aggressively recognise these as symbolically
# real. This second attempt at a cast is a way of unifying the behaviour to the
# more expected form for our users.
cval = complex(self)
if cval.imag == 0.0:
return cval.real
except TypeError:
pass
# In symengine, if an expression was complex at any time, its type is likely to have
# stayed "complex" even when the imaginary part symbolically (i.e. exactly)
# cancelled out. Sympy tends to more aggressively recognise these as symbolically
# real. This second attempt at a cast is a way of unifying the behaviour to the
# more expected form for our users.
cval = complex(self)
if cval.imag == 0.0:
return cval.real
raise TypeError("could not cast expression to float") from exc

def __int__(self):
try:
return int(self._symbol_expr)
# TypeError is for sympy, RuntimeError for symengine
except (TypeError, RuntimeError) as exc:
# TypeError is for backwards compatibility, RuntimeError is raised by symengine
except RuntimeError as exc:
if self.parameters:
raise TypeError(
"ParameterExpression with unbound parameters ({}) "
Expand All @@ -528,14 +441,7 @@ def __deepcopy__(self, memo=None):

def __abs__(self):
"""Absolute of a ParameterExpression"""
if _optionals.HAS_SYMENGINE:
import symengine

return self._call(symengine.Abs)
else:
from sympy import Abs as _abs

return self._call(_abs)
return self._call(symengine.Abs)

def abs(self):
"""Absolute of a ParameterExpression"""
Expand All @@ -553,12 +459,9 @@ def __eq__(self, other):
if isinstance(other, ParameterExpression):
if self.parameters != other.parameters:
return False
if _optionals.HAS_SYMENGINE:
from sympy import sympify
from sympy import sympify

return sympify(self._symbol_expr).equals(sympify(other._symbol_expr))
else:
return self._symbol_expr.equals(other._symbol_expr)
return sympify(self._symbol_expr).equals(sympify(other._symbol_expr))
elif isinstance(other, numbers.Number):
return len(self.parameters) == 0 and complex(self._symbol_expr) == other
return False
Expand All @@ -568,7 +471,7 @@ def is_real(self):

# workaround for symengine behavior that const * (0 + 1 * I) is not real
# see https://github.com/symengine/symengine.py/issues/414
if _optionals.HAS_SYMENGINE and self._symbol_expr.is_real is None:
if self._symbol_expr.is_real is None:
symbol_expr = self._symbol_expr.evalf()
else:
symbol_expr = self._symbol_expr
Expand All @@ -579,9 +482,8 @@ def is_real(self):
# but the parameter will evaluate as real. Check that if the
# expression's is_real attribute returns false that we have a
# non-zero imaginary
if _optionals.HAS_SYMENGINE:
if symbol_expr.imag == 0.0:
return True
if symbol_expr.imag == 0.0:
return True
return False
return symbol_expr.is_real

Expand Down
Loading

0 comments on commit 9a1d01b

Please sign in to comment.