diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 2b9f94d280fd..52e04ec7a990 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -198,7 +198,7 @@ Tier 1 platforms are currently: * Linux x86_64 (distributions compatible with the `manylinux 2014 `__ packaging specification). - * macOS x86_64 (10.9 or newer) + * macOS x86_64 (10.12 or newer) * Windows 64 bit Tier 2 @@ -211,10 +211,6 @@ functioning Python environment. Tier 2 platforms are currently: - * Linux i686 (distributions compatible with the - `manylinux 2014 `__ packaging - specification) for Python < 3.10 - * Windows 32 bit for Python < 3.10 * Linux aarch64 (distributions compatible with the `manylinux 2014 `__ packaging specification) @@ -240,8 +236,8 @@ Tier 3 platforms are currently: * macOS arm64 (10.15 or newer) * Linux i686 (distributions compatible with the `manylinux 2014 `__ packaging - specification) for Python >= 3.10 - * Windows 32 bit for Python >= 3.10 + specification) + * Windows 32 bit Ready to get going?... ====================== diff --git a/pyproject.toml b/pyproject.toml index 25ff0a5dade7..172c0625d25b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 @@ -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] diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index e354a708eace..42ffad5ebf0a 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -17,8 +17,9 @@ from uuid import uuid4, UUID +import symengine + from qiskit.circuit.exceptions import CircuitError -from qiskit.utils import optionals as _optionals from .parameterexpression import ParameterExpression @@ -75,14 +76,7 @@ def __init__( """ self._name = name self._uuid = uuid4() if uuid is None else uuid - if not _optionals.HAS_SYMENGINE: - from sympy import Symbol - - symbol = Symbol(name) - else: - import symengine - - symbol = symengine.Symbol(name) + symbol = symengine.Symbol(name) self._symbol_expr = symbol self._parameter_keys = frozenset((self._hash_key(),)) @@ -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``.""" diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 6b88cede34e1..790fc29b8135 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -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 @@ -69,14 +69,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": @@ -185,15 +180,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. @@ -306,15 +293,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. @@ -367,102 +346,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)})" @@ -494,24 +410,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 ({}) " @@ -530,14 +443,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""" @@ -555,12 +461,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 @@ -570,7 +473,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 @@ -581,9 +484,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 diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 9bc18c9c7f13..a943bb1acf60 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -23,19 +23,14 @@ from copy import deepcopy import numpy as np +import symengine as sym from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse.exceptions import PulseError from qiskit.pulse.library.pulse import Pulse from qiskit.pulse.library.waveform import Waveform -from qiskit.utils import optionals as _optional from qiskit.utils.deprecation import deprecate_arg -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym - def _lifted_gaussian( t: sym.Symbol, @@ -183,34 +178,31 @@ def __set__(self, instance, value): continue params.append(p) - if _optional.HAS_SYMENGINE: - try: - lamb = sym.lambdify(params, [value], real=False) - - def _wrapped_lamb(*args): - if isinstance(args[0], np.ndarray): - # When the args[0] is a vector ("t"), tile other arguments args[1:] - # to prevent evaluation from looping over each element in t. - t = args[0] - args = np.hstack( - ( - t.reshape(t.size, 1), - np.tile(args[1:], t.size).reshape(t.size, len(args) - 1), - ) + try: + lamb = sym.lambdify(params, [value], real=False) + + def _wrapped_lamb(*args): + if isinstance(args[0], np.ndarray): + # When the args[0] is a vector ("t"), tile other arguments args[1:] + # to prevent evaluation from looping over each element in t. + t = args[0] + args = np.hstack( + ( + t.reshape(t.size, 1), + np.tile(args[1:], t.size).reshape(t.size, len(args) - 1), ) - return lamb(args) - - func = _wrapped_lamb - except RuntimeError: - # Currently symengine doesn't support complex_double version for - # several functions such as comparison operator and piecewise. - # If expression contains these function, it fall back to sympy lambdify. - # See https://github.com/symengine/symengine.py/issues/406 for details. - import sympy - - func = sympy.lambdify(params, value) - else: - func = sym.lambdify(params, value) + ) + return lamb(args) + + func = _wrapped_lamb + except RuntimeError: + # Currently symengine doesn't support complex_double version for + # several functions such as comparison operator and piecewise. + # If expression contains these function, it fall back to sympy lambdify. + # See https://github.com/symengine/symengine.py/issues/406 for details. + import sympy + + func = sympy.lambdify(params, value) self.lambda_funcs[key] = func diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 25ca9388bb4e..ce22702c89da 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -19,6 +19,10 @@ from io import BytesIO import numpy as np +import symengine as sym +from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module + load_basic, +) from qiskit.exceptions import QiskitError from qiskit.pulse import library, channels, instructions @@ -26,14 +30,8 @@ from qiskit.qpy import formats, common, type_keys from qiskit.qpy.binary_io import value from qiskit.qpy.exceptions import QpyError -from qiskit.utils import optionals as _optional from qiskit.pulse.configuration import Kernel, Discriminator -if _optional.HAS_SYMENGINE: - import symengine as sym -else: - import sympy as sym - def _read_channel(file_obj, version): type_key = common.read_type_key(file_obj) @@ -106,23 +104,15 @@ def _read_discriminator(file_obj, version): def _loads_symbolic_expr(expr_bytes, use_symengine=False): if expr_bytes == b"": return None + expr_bytes = zlib.decompress(expr_bytes) if use_symengine: - _optional.HAS_SYMENGINE.require_now("load a symengine expression") - from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module - load_basic, - ) - - expr = load_basic(zlib.decompress(expr_bytes)) + return load_basic(expr_bytes) else: from sympy import parse_expr - expr_txt = zlib.decompress(expr_bytes).decode(common.ENCODE) + expr_txt = expr_bytes.decode(common.ENCODE) expr = parse_expr(expr_txt) - if _optional.HAS_SYMENGINE: - from symengine import sympify - - return sympify(expr) - return expr + return sym.sympify(expr) def _read_symbolic_pulse(file_obj, version): @@ -404,7 +394,6 @@ def _dumps_symbolic_expr(expr, use_symengine): if expr is None: return b"" if use_symengine: - _optional.HAS_SYMENGINE.require_now("dump a symengine expression") expr_bytes = expr.__reduce__()[1][0] else: from sympy import srepr, sympify diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 7bae82c6911f..686e72c9a4e6 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -19,6 +19,11 @@ import uuid import numpy as np +import symengine +from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module + load_basic, +) + from qiskit.circuit import CASE_DEFAULT, Clbit, ClassicalRegister from qiskit.circuit.classical import expr, types @@ -26,7 +31,6 @@ from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement from qiskit.qpy import common, formats, exceptions, type_keys -from qiskit.utils import optionals as _optional def _write_parameter(file_obj, obj): @@ -51,7 +55,6 @@ def _write_parameter_vec(file_obj, obj): def _write_parameter_expression(file_obj, obj, use_symengine): if use_symengine: - _optional.HAS_SYMENGINE.require_now("write_parameter_expression") expr_bytes = obj._symbol_expr.__reduce__()[1][0] else: from sympy import srepr, sympify @@ -224,13 +227,7 @@ def _read_parameter_expression(file_obj): ) from sympy.parsing.sympy_parser import parse_expr - if _optional.HAS_SYMENGINE: - from symengine import sympify - - expr_ = sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) - else: - expr_ = parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE)) - + expr_ = symengine.sympify(parse_expr(file_obj.read(data.expr_size).decode(common.ENCODE))) symbol_map = {} for _ in range(data.map_elements): elem_data = formats.PARAM_EXPR_MAP_ELEM( @@ -264,23 +261,14 @@ def _read_parameter_expression_v3(file_obj, vectors, use_symengine): data = formats.PARAMETER_EXPR( *struct.unpack(formats.PARAMETER_EXPR_PACK, file_obj.read(formats.PARAMETER_EXPR_SIZE)) ) - from sympy.parsing.sympy_parser import parse_expr payload = file_obj.read(data.expr_size) if use_symengine: - _optional.HAS_SYMENGINE.require_now("read_parameter_expression_v3") - from symengine.lib.symengine_wrapper import ( # pylint: disable = no-name-in-module - load_basic, - ) - expr_ = load_basic(payload) else: - if _optional.HAS_SYMENGINE: - from symengine import sympify + from sympy.parsing.sympy_parser import parse_expr - expr_ = sympify(parse_expr(payload.decode(common.ENCODE))) - else: - expr_ = parse_expr(payload.decode(common.ENCODE)) + expr_ = symengine.sympify(parse_expr(payload.decode(common.ENCODE))) symbol_map = {} for _ in range(data.map_elements): diff --git a/qiskit/qpy/interface.py b/qiskit/qpy/interface.py index f4a77ec14598..6de673afbc0f 100644 --- a/qiskit/qpy/interface.py +++ b/qiskit/qpy/interface.py @@ -75,7 +75,7 @@ def dump( programs: Union[List[QPY_SUPPORTED_TYPES], QPY_SUPPORTED_TYPES], file_obj: BinaryIO, metadata_serializer: Optional[Type[JSONEncoder]] = None, - use_symengine: bool = False, + use_symengine: bool = True, ): """Write QPY binary data to a file diff --git a/releasenotes/notes/platform-support-f7f693aaf5dec044.yaml b/releasenotes/notes/platform-support-f7f693aaf5dec044.yaml new file mode 100644 index 000000000000..f52aef82c80c --- /dev/null +++ b/releasenotes/notes/platform-support-f7f693aaf5dec044.yaml @@ -0,0 +1,29 @@ +--- +upgrade: + - | + The ``symengine`` library is now a hard requirement for all platforms. + Previously, ``symengine`` was required only on platforms that had + pre-compiled packages available and ``sympy`` would be used as a fallback + if it wasn't installed. These split requirements were resulting in increased + complexity, as it was necessary to determine which libraries were installed + to debug an issue. Requiring ``symengine`` for all systems greatly decreases + the complexity and optimizes Qiskit for higher performance. However, + users on i686 Linux, 32 bit Windows, and s390x Linux (the platforms without + precompiled packages on PyPI) will need to build symengine from source. + - | + Support for 32 bit platforms, i686 Linux and 32 bit Windows, on + Python < 3.10 has been downgraded from Tier 2 to Tier 3, as documented in + the :ref:`platform_support` page. This is a consequence of making + ``symengine`` required for all users, as there is a lack of pre-compiled packages + available for these platforms, so users will need to build symengine from + source. + - | + For macOS users, the minimum version of macOS is now 10.12. Previously, the + precompiled binary wheel packages for macOS x86_64 were published with + support for >=10.9. However, because of changes in the + `support policy `__ + for the Rust programming language the minimum version needed to raised + to macOS 10.12. If you're using Qiskit on macOS 10.9 you can probably + build Qiskit from source while the Qiskit MSRV (minimum supported Rust + version) is < 1.74, but the precompiled binaries published to PyPI will + only be compatible with macOS >= 10.12. diff --git a/requirements.txt b/requirements.txt index d41eee50ce95..7620b00ee3ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,4 @@ dill>=0.3 python-dateutil>=2.8.0 stevedore>=3.0.0 typing-extensions; python_version<'3.11' -# symengine pinning needed due lowered precision handling complex -# multiplication in version 0.10 wich breaks parameter assignment test -# (can be removed once issue is fix) -symengine>=0.9, <0.10; platform_machine == 'x86_64' or platform_machine == 'aarch64' or platform_machine == 'ppc64le' or platform_machine == 'amd64' or platform_machine == 'arm64' +symengine>=0.9, <0.10 diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 4f8523358dbd..5695a298f782 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -55,7 +55,6 @@ from qiskit.quantum_info.random import random_unitary from qiskit.circuit.controlledgate import ControlledGate from qiskit.utils import optionals -from qiskit.exceptions import MissingOptionalLibraryError @ddt.ddt @@ -1747,22 +1746,3 @@ def test_symengine_full_path(self): new_circ = load(qpy_file)[0] self.assertEqual(self.qc, new_circ) self.assertDeprecatedBitProperties(self.qc, new_circ) - - @unittest.skipIf(not optionals.HAS_SYMENGINE, "Install symengine to run this test.") - def test_dump_no_symengine(self): - """Test dump fails if symengine is not installed and use_symengine==True.""" - qpy_file = io.BytesIO() - with optionals.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(MissingOptionalLibraryError): - dump(self.qc, qpy_file, use_symengine=True) - - @unittest.skipIf(not optionals.HAS_SYMENGINE, "Install symengine to run this test.") - def test_load_no_symengine(self): - """Test that load fails if symengine is not installed and the - file was created with use_symengine==True.""" - qpy_file = io.BytesIO() - dump(self.qc, qpy_file, use_symengine=True) - qpy_file.seek(0) - with optionals.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(MissingOptionalLibraryError): - _ = load(qpy_file)[0] diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 8137ef21a6bb..321e018d5a64 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -37,7 +37,6 @@ ) from qiskit.pulse.instructions import Play, TimeBlockade from qiskit.circuit import Parameter, QuantumCircuit, Gate -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.test import QiskitTestCase from qiskit.qpy import dump, load from qiskit.utils import optionals as _optional @@ -460,22 +459,3 @@ def test_symengine_full_path(self): qpy_file.seek(0) new_sched = load(qpy_file)[0] self.assertEqual(self.test_sched, new_sched) - - @unittest.skipIf(not _optional.HAS_SYMENGINE, "Install symengine to run this test.") - def test_dump_no_symengine(self): - """Test dump fails if symengine is not installed and use_symengine==True.""" - qpy_file = io.BytesIO() - with _optional.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(MissingOptionalLibraryError): - dump(self.test_sched, qpy_file, use_symengine=True) - - @unittest.skipIf(not _optional.HAS_SYMENGINE, "Install symengine to run this test.") - def test_load_no_symengine(self): - """Test that load fails if symengine is not installed and the - file was created with use_symengine==True.""" - qpy_file = io.BytesIO() - dump(self.test_sched, qpy_file, use_symengine=True) - qpy_file.seek(0) - with _optional.HAS_SYMENGINE.disable_locally(): - with self.assertRaises(MissingOptionalLibraryError): - _ = load(qpy_file)[0]