diff --git a/qiskit/circuit/delay.py b/qiskit/circuit/delay.py index 624455428101..c9f862c5f8a7 100644 --- a/qiskit/circuit/delay.py +++ b/qiskit/circuit/delay.py @@ -77,7 +77,7 @@ def validate_parameter(self, parameter): elif isinstance(parameter, ParameterExpression): if len(parameter.parameters) > 0: return parameter # expression has free parameters, we cannot validate it - if not parameter._symbol_expr.is_real: + if not parameter.is_real(): raise CircuitError(f"Bound parameter expression is complex in delay {self.name}") fval = float(parameter) if self.unit == "dt": diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index e4619cffea10..fc2617ea527f 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -17,7 +17,7 @@ import numpy as np from scipy.linalg import schur -from qiskit.circuit.parameter import ParameterExpression +from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.exceptions import CircuitError from .instruction import Instruction @@ -249,10 +249,9 @@ def validate_parameter(self, parameter): if isinstance(parameter, ParameterExpression): if len(parameter.parameters) > 0: return parameter # expression has free parameters, we cannot validate it - if not parameter._symbol_expr.is_real: - raise CircuitError( - "Bound parameter expression is complex in gate {}".format(self.name) - ) + if not parameter.is_real(): + msg = "Bound parameter expression is complex in gate {}".format(self.name) + raise CircuitError(msg) return parameter # per default assume parameters must be real when bound if isinstance(parameter, (int, float)): return parameter diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 139fd72a4d47..f8dfc313bbea 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -17,6 +17,13 @@ from .parameterexpression import ParameterExpression +try: + import symengine + + HAS_SYMENGINE = True +except ImportError: + HAS_SYMENGINE = False + class Parameter(ParameterExpression): """Parameter Class for variable parameters.""" @@ -49,10 +56,12 @@ def __init__(self, name: str): be any unicode string, e.g. "ϕ". """ self._name = name + if not HAS_SYMENGINE: + from sympy import Symbol - from sympy import Symbol - - symbol = Symbol(name) + symbol = Symbol(name) + else: + symbol = symengine.Symbol(name) super().__init__(symbol_map={self: symbol}, expr=symbol) def subs(self, parameter_map: dict): @@ -86,3 +95,16 @@ def __eq__(self, other): def __hash__(self): return self._hash + + def __getstate__(self): + return {"name": self._name} + + def __setstate__(self, state): + self._name = state["name"] + if not HAS_SYMENGINE: + from sympy import Symbol + + symbol = Symbol(self._name) + else: + symbol = symengine.Symbol(self._name) + super().__init__(symbol_map={self: symbol}, expr=symbol) diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index c44c93dc8e21..5430a345a76e 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -21,6 +21,14 @@ from qiskit.circuit.exceptions import CircuitError +try: + import symengine + + HAS_SYMENGINE = True +except ImportError: + HAS_SYMENGINE = False + + ParameterValueType = Union["ParameterExpression", float, int] @@ -53,7 +61,12 @@ def parameters(self) -> Set: def conjugate(self) -> "ParameterExpression": """Return the conjugate.""" - conjugated = ParameterExpression(self._parameter_symbols, self._symbol_expr.conjugate()) + if HAS_SYMENGINE: + conjugated = ParameterExpression( + self._parameter_symbols, symengine.conjugate(self._symbol_expr) + ) + else: + conjugated = ParameterExpression(self._parameter_symbols, self._symbol_expr.conjugate()) return conjugated def assign(self, parameter, value: ParameterValueType) -> "ParameterExpression": @@ -110,7 +123,9 @@ def bind(self, parameter_values: Dict) -> "ParameterExpression": p: s for p, s in self._parameter_symbols.items() if p in free_parameters } - if bound_symbol_expr.is_infinite: + if ( + hasattr(bound_symbol_expr, "is_infinite") and bound_symbol_expr.is_infinite + ) or bound_symbol_expr == float("inf"): raise ZeroDivisionError( "Binding provided for expression " "results in division by zero " @@ -142,10 +157,12 @@ def subs(self, parameter_map: Dict) -> "ParameterExpression": self._raise_if_passed_unknown_parameters(parameter_map.keys()) self._raise_if_parameter_names_conflict(inbound_parameters, parameter_map.keys()) + if HAS_SYMENGINE: + new_parameter_symbols = {p: symengine.Symbol(p.name) for p in inbound_parameters} + else: + from sympy import Symbol - from sympy import Symbol - - new_parameter_symbols = {p: Symbol(p.name) for p in inbound_parameters} + new_parameter_symbols = {p: Symbol(p.name) for p in inbound_parameters} # Include existing parameters in self not set to be replaced. new_parameter_symbols.update( @@ -257,11 +274,14 @@ def gradient(self, param) -> Union["ParameterExpression", float]: return 0.0 # Compute the gradient of the parameter expression w.r.t. param - import sympy as sy - key = self._parameter_symbols[param] - # TODO enable nth derivative - expr_grad = sy.Derivative(self._symbol_expr, key).doit() + if HAS_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() # generate the new dictionary of symbols # this needs to be done since in the derivative some symbols might disappear (e.g. @@ -310,57 +330,83 @@ def _call(self, ufunc): def sin(self): """Sine of a ParameterExpression""" - from sympy import sin as _sin + if HAS_SYMENGINE: + return self._call(symengine.sin) + else: + from sympy import sin as _sin - return self._call(_sin) + return self._call(_sin) def cos(self): """Cosine of a ParameterExpression""" - from sympy import cos as _cos + if HAS_SYMENGINE: + return self._call(symengine.cos) + else: + from sympy import cos as _cos - return self._call(_cos) + return self._call(_cos) def tan(self): """Tangent of a ParameterExpression""" - from sympy import tan as _tan + if HAS_SYMENGINE: + return self._call(symengine.tan) + else: + from sympy import tan as _tan - return self._call(_tan) + return self._call(_tan) def arcsin(self): """Arcsin of a ParameterExpression""" - from sympy import asin as _asin + if HAS_SYMENGINE: + return self._call(symengine.asin) + else: + from sympy import asin as _asin - return self._call(_asin) + return self._call(_asin) def arccos(self): """Arccos of a ParameterExpression""" - from sympy import acos as _acos + if HAS_SYMENGINE: + return self._call(symengine.acos) + else: + from sympy import acos as _acos - return self._call(_acos) + return self._call(_acos) def arctan(self): """Arctan of a ParameterExpression""" - from sympy import atan as _atan + if HAS_SYMENGINE: + return self._call(symengine.atan) + else: + from sympy import atan as _atan - return self._call(_atan) + return self._call(_atan) def exp(self): """Exponential of a ParameterExpression""" - from sympy import exp as _exp + if HAS_SYMENGINE: + return self._call(symengine.exp) + else: + from sympy import exp as _exp - return self._call(_exp) + return self._call(_exp) def log(self): """Logarithm of a ParameterExpression""" - from sympy import log as _log + if HAS_SYMENGINE: + return self._call(symengine.log) + else: + from sympy import log as _log - return self._call(_log) + return self._call(_log) def __repr__(self): return "{}({})".format(self.__class__.__name__, str(self)) def __str__(self): - return str(self._symbol_expr) + from sympy import sympify + + return str(sympify(self._symbol_expr)) def __float__(self): if self.parameters: @@ -405,9 +451,56 @@ def __eq__(self, other): bool: result of the comparison """ if isinstance(other, ParameterExpression): - return self.parameters == other.parameters and self._symbol_expr.equals( - other._symbol_expr - ) + if self.parameters != other.parameters: + return False + if HAS_SYMENGINE: + from sympy import sympify + + return sympify(self._symbol_expr).equals(sympify(other._symbol_expr)) + else: + return self._symbol_expr.equals(other._symbol_expr) elif isinstance(other, numbers.Number): return len(self.parameters) == 0 and complex(self._symbol_expr) == other return False + + def __getstate__(self): + if HAS_SYMENGINE: + from sympy import sympify + + symbols = {k: sympify(v) for k, v in self._parameter_symbols.items()} + expr = sympify(self._symbol_expr) + return {"type": "symengine", "symbols": symbols, "expr": expr, "names": self._names} + else: + return { + "type": "sympy", + "symbols": self._parameter_symbols, + "expr": self._symbol_expr, + "names": self._names, + } + + def __setstate__(self, state): + if state["type"] == "symengine": + self._symbol_expr = symengine.sympify(state["expr"]) + self._parameter_symbols = {k: symengine.sympify(v) for k, v in state["symbols"].items()} + self._parameters = set(self._parameter_symbols) + else: + self._symbol_expr = state["expr"] + self._parameter_symbols = state["symbols"] + self._parameters = set(self._parameter_symbols) + self._names = state["names"] + + def is_real(self): + """Return whether the expression is real""" + + if not self._symbol_expr.is_real and self._symbol_expr.is_real is not None: + # Symengine returns false for is_real on the expression if + # there is a imaginary component (even if that component is 0), + # 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 HAS_SYMENGINE: + if self._symbol_expr.imag != 0.0: + return False + else: + return False + return True diff --git a/qiskit/circuit/parametervector.py b/qiskit/circuit/parametervector.py index c3878063120d..8d51e2984378 100644 --- a/qiskit/circuit/parametervector.py +++ b/qiskit/circuit/parametervector.py @@ -50,6 +50,21 @@ def vector(self): """Get the parent vector instance.""" return self._vector + def __getstate__(self): + return { + "name": self._name, + "uuid": self._uuid, + "vector": self._vector, + "index": self._index, + } + + def __setstate__(self, state): + self._name = state["name"] + self._uuid = state["uuid"] + self._vector = state["vector"] + self._index = state["index"] + super().__init__(self._name) + class ParameterVector: """ParameterVector class to quickly generate lists of parameters.""" diff --git a/qiskit/circuit/tools/pi_check.py b/qiskit/circuit/tools/pi_check.py index 6bb2a07f8894..5569475b7bd8 100644 --- a/qiskit/circuit/tools/pi_check.py +++ b/qiskit/circuit/tools/pi_check.py @@ -46,7 +46,13 @@ def pi_check(inpt, eps=1e-6, output="text", ndigits=5): """ if isinstance(inpt, ParameterExpression): param_str = str(inpt) - syms = inpt._symbol_expr.expr_free_symbols + if not hasattr(inpt._symbol_expr, "expr_free_symbols"): + from sympy import sympify + + expr = sympify(inpt._symbol_expr) + else: + expr = inpt._symbol_expr + syms = expr.expr_free_symbols for sym in syms: if not sym.is_number: continue diff --git a/releasenotes/notes/symengine-2fa0479fa7d9aa80.yaml b/releasenotes/notes/symengine-2fa0479fa7d9aa80.yaml new file mode 100644 index 000000000000..dd977ebae0da --- /dev/null +++ b/releasenotes/notes/symengine-2fa0479fa7d9aa80.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + A new requirement `symengine `__ has + been added for Linux (on x86_64, aarch64, and ppc64le) and macOS users + (x86_64 and arm64). It is an optional dependency on Windows (and available + on PyPi as a precompiled package for 64bit Windows) and other + architectures. If it is installed it provides significantly improved + performance for the evaluation of :class:`~qiskit.circuit.Parameter` and + :class:`~qiskit.circuit.ParameterExpression` objects. diff --git a/requirements.txt b/requirements.txt index e75908889a19..964c637b770f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ dill>=0.3 fastjsonschema>=2.10 python-constraint>=1.4 python-dateutil>=2.8.0 +symengine>0.7 ; platform_machine == 'x86_64' or platform_machine == 'aarch64' or platform_machine == 'ppc64le' or platform_machine == 'amd64' or platform_machine == 'arm64' diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index f3f7a3294010..9eb46bba8f8a 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -12,6 +12,8 @@ """Test circuits with variable parameters.""" import unittest +import cmath +import math import copy import pickle @@ -604,9 +606,8 @@ def test_circuit_generation(self): circs.append(getattr(qc_aer, assign_fun)({theta: theta_i})) qobj = assemble(circs) for index, theta_i in enumerate(theta_list): - self.assertEqual( - float(qobj.experiments[index].instructions[0].params[0]), theta_i - ) + res = float(qobj.experiments[index].instructions[0].params[0]) + self.assertTrue(math.isclose(res, theta_i), "%s != %s" % (res, theta_i)) def test_circuit_composition(self): """Test preservation of parameters when combining circuits.""" @@ -1254,7 +1255,9 @@ def test_expressions_of_parameter_with_constant(self): expr = op(x, const) bound_expr = expr.bind({x: 2.3}) - self.assertEqual(complex(bound_expr), op(2.3, const)) + res = complex(bound_expr) + expected = op(2.3, const) + self.assertTrue(cmath.isclose(res, expected), "%s != %s" % (res, expected)) def test_complex_parameter_bound_to_real(self): """Test a complex parameter expression can be real if bound correctly.""" diff --git a/test/python/opflow/test_aer_pauli_expectation.py b/test/python/opflow/test_aer_pauli_expectation.py index 222516a57fb8..dcd236d53915 100644 --- a/test/python/opflow/test_aer_pauli_expectation.py +++ b/test/python/opflow/test_aer_pauli_expectation.py @@ -181,7 +181,10 @@ def generate_parameters(num): def validate_sampler(ideal, sut, param_bindings): expect_sampled = ideal.convert(expect_op, params=param_bindings).eval() actual_sampled = sut.convert(expect_op, params=param_bindings).eval() - self.assertAlmostEqual(actual_sampled, expect_sampled, delta=0.1) + self.assertTrue( + np.allclose(actual_sampled, expect_sampled), + "%s != %s" % (actual_sampled, expect_sampled), + ) def get_circuit_templates(sampler): return sampler._transpiled_circ_templates diff --git a/test/python/opflow/test_gradients.py b/test/python/opflow/test_gradients.py index 88499d645b13..1a9b3bb6b11e 100644 --- a/test/python/opflow/test_gradients.py +++ b/test/python/opflow/test_gradients.py @@ -18,7 +18,6 @@ from itertools import product import numpy as np from ddt import ddt, data, idata, unpack -from sympy import Symbol, cos try: import jax.numpy as jnp @@ -288,10 +287,7 @@ def test_state_gradient3(self, method): a = Parameter("a") # b = Parameter('b') params = a - x = Symbol("x") - expr = cos(x) + 1 - c = ParameterExpression({a: x}, expr) - + c = np.cos(a) + 1 q = QuantumRegister(1) qc = QuantumCircuit(q) qc.h(q) diff --git a/test/python/pulse/test_parameters.py b/test/python/pulse/test_parameters.py index 77625b65cc96..34201373bf23 100644 --- a/test/python/pulse/test_parameters.py +++ b/test/python/pulse/test_parameters.py @@ -12,12 +12,14 @@ """Test cases for parameters used in Schedules.""" import unittest +import cmath from copy import deepcopy import numpy as np from qiskit import pulse, assemble from qiskit.circuit import Parameter +from qiskit.circuit.parameterexpression import HAS_SYMENGINE from qiskit.pulse import PulseError from qiskit.pulse.channels import DriveChannel, AcquireChannel, MemorySlot from qiskit.pulse.transforms import inline_subroutines @@ -349,7 +351,7 @@ def test_assign_parameter_to_subroutine_parameter(self): subroutine = pulse.Schedule() subroutine += pulse.Play(waveform, DriveChannel(0)) - reference = deepcopy(subroutine).assign_parameters({param1: 0.1 * np.exp(1j * 0.5)}) + reference = deepcopy(subroutine).assign_parameters({param1: 0.1 * np.exp(0.5j)}) main_prog = pulse.Schedule() pdict = {param1: param_sub1 * np.exp(1j * param_sub2)} @@ -358,8 +360,19 @@ def test_assign_parameter_to_subroutine_parameter(self): # parameter is overwritten by parameters self.assertEqual(len(main_prog.parameters), 2) target = deepcopy(main_prog).assign_parameters({param_sub1: 0.1, param_sub2: 0.5}) - - self.assertEqual(inline_subroutines(target), reference) + result = inline_subroutines(target) + if not HAS_SYMENGINE: + self.assertEqual(result, reference) + else: + # Because of simplification differences between sympy and symengine when + # symengine is used we get 0.1*exp(0.5*I) instead of the evaluated + # 0.0877582562 + 0.0479425539*I resulting in a failure. When + # symengine is installed manually build the amplitude as a complex to + # avoid this. + reference = pulse.Schedule() + waveform = pulse.library.Constant(duration=100, amp=0.1 * cmath.exp(0.5j)) + reference += pulse.Play(waveform, DriveChannel(0)) + self.assertEqual(result, reference) class TestParameterDuration(QiskitTestCase):