diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 074023efb412..48486e66373d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,10 @@ contributing to terra, these are documented below. * [Branches](#branches) * [Release Cycle](#release-cycle) * [Adding deprecation warnings](#adding-deprecation-warnings) +* [Using dependencies](#using-dependencies) + * [Adding a requirement](#adding-a-requirement) + * [Adding an optional dependency](#adding-an-optional-dependency) + * [Checking for optionals](#checking-for-optionals) * [Dealing with git blame ignore list](#dealing-with-the-git-blame-ignore-list) ### Choose an issue to work on @@ -478,6 +482,37 @@ def test_method2(self): `test_method1_deprecated` can be removed after `Obj.method1` is removed (following the [Qiskit Deprecation Policy](https://qiskit.org/documentation/contributing_to_qiskit.html#deprecation-policy)). +## Using dependencies + +We distinguish between "requirements" and "optional dependencies" in qiskit-terra. +A requirement is a package that is absolutely necessary for core functionality in qiskit-terra, such as Numpy or Scipy. +An optional dependency is a package that is used for specialized functionality, which might not be needed by all users. +If a new feature has a new dependency, it is almost certainly optional. + +### Adding a requirement + +Any new requirement must have broad system support; it needs to be supported on all the Python versions and operating systems that qiskit-terra supports. +It also cannot impose many version restrictions on other packages. +Users often install qiskit-terra into virtual environments with many different packages in, and we need to ensure that neither we, nor any of our requirements, conflict with their other packages. +When adding a new requirement, you must add it to [`requirements.txt`](requirements.txt) with as loose a constraint on the allowed versions as possible. + +### Adding an optional dependency + +New features can also use optional dependencies, which might be used only in very limited parts of qiskit-terra. +These are not required to use the rest of the package, and so should not be added to `requirements.txt`. +Instead, if several optional dependencies are grouped together to provide one feature, you can consider adding an "extra" to the package metadata, such as the `visualization` extra that installs Matplotlib and Seaborn (amongst others). +To do this, modify the [`setup.py`](setup.py) file, adding another entry in the `extras_require` keyword argument to `setup()` at the bottom of the file. +You do not need to be quite as accepting of all versions here, but it is still a good idea to be as permissive as you possibly can be. +You should also add a new "tester" to [`qiskit.utils.optionals`](qiskit/utils/optionals.py), for use in the next section. + +### Checking for optionals + +You cannot `import` an optional dependency at the top of a file, because if it is not installed, it will raise an error and qiskit-terra will be unusable. +We also largely want to avoid importing packages until they are actually used; if we import a lot of packages during `import qiskit`, it becomes sluggish for the user if they have a large environment. +Instead, you should use [one of the "lazy testers" for optional dependencies](https://qiskit.org/documentation/apidoc/utils.html#module-qiskit.utils.optionals), and import your optional dependency inside the function or class that uses it, as in the examples within that link. +Very lightweight _requirements_ can be imported at the tops of files, but even this should be limited; it's always ok to `import numpy`, but Scipy modules are relatively heavy, so only import them within functions that use them. + + ## Dealing with the git blame ignore list In the qiskit-terra repository we maintain a list of commits for git blame diff --git a/qiskit/algorithms/optimizers/bobyqa.py b/qiskit/algorithms/optimizers/bobyqa.py index bbf7c92eee46..623e2fad042e 100644 --- a/qiskit/algorithms/optimizers/bobyqa.py +++ b/qiskit/algorithms/optimizers/bobyqa.py @@ -15,17 +15,11 @@ from typing import Any, Dict, Tuple, List, Callable, Optional import numpy as np -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT -try: - import skquant.opt as skq - - _HAS_SKQUANT = True -except ImportError: - _HAS_SKQUANT = False - +@_optionals.HAS_SKQUANT.require_in_instance class BOBYQA(Optimizer): """Bound Optimization BY Quadratic Approximation algorithm. @@ -48,10 +42,6 @@ def __init__( Raises: MissingOptionalLibraryError: scikit-quant not installed """ - if not _HAS_SKQUANT: - raise MissingOptionalLibraryError( - libname="scikit-quant", name="BOBYQA", pip_install="pip install scikit-quant" - ) super().__init__() self._maxiter = maxiter @@ -74,6 +64,8 @@ def minimize( jac: Optional[Callable[[POINT], POINT]] = None, bounds: Optional[List[Tuple[float, float]]] = None, ) -> OptimizerResult: + from skquant import opt as skq + res, history = skq.minimize( func=fun, x0=np.asarray(x0), diff --git a/qiskit/algorithms/optimizers/imfil.py b/qiskit/algorithms/optimizers/imfil.py index bb15f4d74164..b4cbfd0e586c 100644 --- a/qiskit/algorithms/optimizers/imfil.py +++ b/qiskit/algorithms/optimizers/imfil.py @@ -14,17 +14,11 @@ from typing import Any, Dict, Callable, Optional, List, Tuple -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT -try: - import skquant.opt as skq - - _HAS_SKQUANT = True -except ImportError: - _HAS_SKQUANT = False - +@_optionals.HAS_SKQUANT.require_in_instance class IMFIL(Optimizer): """IMplicit FILtering algorithm. @@ -49,10 +43,6 @@ def __init__( Raises: MissingOptionalLibraryError: scikit-quant not installed """ - if not _HAS_SKQUANT: - raise MissingOptionalLibraryError( - libname="scikit-quant", name="IMFIL", pip_install="pip install scikit-quant" - ) super().__init__() self._maxiter = maxiter @@ -77,6 +67,8 @@ def minimize( jac: Optional[Callable[[POINT], POINT]] = None, bounds: Optional[List[Tuple[float, float]]] = None, ) -> OptimizerResult: + from skquant import opt as skq + res, history = skq.minimize( func=fun, x0=x0, diff --git a/qiskit/algorithms/optimizers/nlopts/nloptimizer.py b/qiskit/algorithms/optimizers/nlopts/nloptimizer.py index 54907705dfe6..18b42a6bedbb 100644 --- a/qiskit/algorithms/optimizers/nlopts/nloptimizer.py +++ b/qiskit/algorithms/optimizers/nlopts/nloptimizer.py @@ -17,24 +17,12 @@ from abc import abstractmethod import logging import numpy as np -from qiskit.exceptions import MissingOptionalLibraryError + +from qiskit.utils import optionals as _optionals from ..optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT logger = logging.getLogger(__name__) -try: - import nlopt - - logger.info( - "NLopt version: %s.%s.%s", - nlopt.version_major(), - nlopt.version_minor(), - nlopt.version_bugfix(), - ) - _HAS_NLOPT = True -except ImportError: - _HAS_NLOPT = False - class NLoptOptimizerType(Enum): """NLopt Valid Optimizer""" @@ -46,11 +34,14 @@ class NLoptOptimizerType(Enum): GN_ISRES = 5 +@_optionals.HAS_NLOPT.require_in_instance class NLoptOptimizer(Optimizer): """ NLopt global optimizer base class """ + # pylint: disable=import-error + _OPTIONS = ["max_evals"] def __init__(self, max_evals: int = 1000) -> None: # pylint: disable=unused-argument @@ -61,15 +52,7 @@ def __init__(self, max_evals: int = 1000) -> None: # pylint: disable=unused-arg Raises: MissingOptionalLibraryError: NLopt library not installed. """ - if not _HAS_NLOPT: - raise MissingOptionalLibraryError( - libname="nlopt", - name="NLoptOptimizer", - msg=( - "See https://qiskit.org/documentation/apidoc/" - "qiskit.algorithms.optimizers.nlopts.html for installation information" - ), - ) + import nlopt super().__init__() for k, v in list(locals().items()): @@ -124,6 +107,8 @@ def minimize( jac: Optional[Callable[[POINT], POINT]] = None, bounds: Optional[List[Tuple[float, float]]] = None, ) -> OptimizerResult: + import nlopt + x0 = np.asarray(x0) if bounds is None: diff --git a/qiskit/algorithms/optimizers/snobfit.py b/qiskit/algorithms/optimizers/snobfit.py index fa6a33cadb7d..041789c38869 100644 --- a/qiskit/algorithms/optimizers/snobfit.py +++ b/qiskit/algorithms/optimizers/snobfit.py @@ -15,25 +15,12 @@ from typing import Any, Dict, Optional, Callable, Tuple, List import numpy as np -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals from .optimizer import Optimizer, OptimizerSupportLevel, OptimizerResult, POINT -try: - import skquant.opt as skq - - _HAS_SKQUANT = True -except ImportError: - _HAS_SKQUANT = False - -try: - from SQSnobFit import optset - - _HAS_SKSNOBFIT = True -except ImportError: - _HAS_SKSNOBFIT = False - - +@_optionals.HAS_SKQUANT.require_in_instance +@_optionals.HAS_SQSNOBFIT.require_in_instance class SNOBFIT(Optimizer): """Stable Noisy Optimization by Branch and FIT algorithm. @@ -64,14 +51,6 @@ def __init__( Raises: MissingOptionalLibraryError: scikit-quant or SQSnobFit not installed """ - if not _HAS_SKQUANT: - raise MissingOptionalLibraryError( - libname="scikit-quant", name="SNOBFIT", pip_install="pip install scikit-quant" - ) - if not _HAS_SKSNOBFIT: - raise MissingOptionalLibraryError( - libname="SQSnobFit", name="SNOBFIT", pip_install="pip install SQSnobFit" - ) super().__init__() self._maxiter = maxiter self._maxfail = maxfail @@ -102,6 +81,9 @@ def minimize( jac: Optional[Callable[[POINT], POINT]] = None, bounds: Optional[List[Tuple[float, float]]] = None, ) -> OptimizerResult: + import skquant.opt as skq + from SQSnobFit import optset + snobfit_settings = { "maxmp": self._maxmp, "maxfail": self._maxfail, diff --git a/qiskit/circuit/gate.py b/qiskit/circuit/gate.py index c3136a84a7ef..eb004685331b 100644 --- a/qiskit/circuit/gate.py +++ b/qiskit/circuit/gate.py @@ -15,7 +15,6 @@ from warnings import warn from typing import List, Optional, Union, Tuple import numpy as np -from scipy.linalg import schur from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.circuit.exceptions import CircuitError @@ -72,6 +71,7 @@ def power(self, exponent: float): """ from qiskit.quantum_info.operators import Operator # pylint: disable=cyclic-import from qiskit.extensions.unitary import UnitaryGate # pylint: disable=cyclic-import + from scipy.linalg import schur # Should be diagonalized because it's a unitary. decomposition, unitary = schur(Operator(self).data, output="complex") diff --git a/qiskit/circuit/parameter.py b/qiskit/circuit/parameter.py index 0daab9fb9f17..558853f11a9b 100644 --- a/qiskit/circuit/parameter.py +++ b/qiskit/circuit/parameter.py @@ -15,15 +15,9 @@ from uuid import uuid4 +from qiskit.utils import optionals as _optionals from .parameterexpression import ParameterExpression -try: - import symengine - - HAS_SYMENGINE = True -except ImportError: - HAS_SYMENGINE = False - class Parameter(ParameterExpression): """Parameter Class for variable parameters. @@ -81,11 +75,13 @@ def __init__(self, name: str): be any unicode string, e.g. "ϕ". """ self._name = name - if not HAS_SYMENGINE: + if not _optionals.HAS_SYMENGINE: from sympy import Symbol symbol = Symbol(name) else: + import symengine + symbol = symengine.Symbol(name) super().__init__(symbol_map={self: symbol}, expr=symbol) @@ -126,10 +122,12 @@ def __getstate__(self): def __setstate__(self, state): self._name = state["name"] - if not HAS_SYMENGINE: + if not _optionals.HAS_SYMENGINE: from sympy import Symbol symbol = Symbol(self._name) else: + import symengine + 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 55d35af2a3bf..175a0557fe70 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -20,13 +20,7 @@ import numpy from qiskit.circuit.exceptions import CircuitError - -try: - import symengine - - HAS_SYMENGINE = True -except ImportError: - HAS_SYMENGINE = False +from qiskit.utils import optionals as _optionals # This type is redefined at the bottom to insert the full reference to "ParameterExpression", so it @@ -71,7 +65,9 @@ def _names(self) -> Dict: def conjugate(self) -> "ParameterExpression": """Return the conjugate.""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + conjugated = ParameterExpression( self._parameter_symbols, symengine.conjugate(self._symbol_expr) ) @@ -170,7 +166,9 @@ def subs(self, parameter_map: Dict) -> "ParameterExpression": self._raise_if_passed_unknown_parameters(parameter_map.keys()) self._raise_if_parameter_names_conflict(inbound_names, parameter_map.keys()) - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + new_parameter_symbols = {p: symengine.Symbol(p.name) for p in inbound_parameters} else: from sympy import Symbol @@ -292,7 +290,9 @@ def gradient(self, param) -> Union["ParameterExpression", complex]: # Compute the gradient of the parameter expression w.r.t. param key = self._parameter_symbols[param] - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + expr_grad = symengine.Derivative(self._symbol_expr, key) else: # TODO enable nth derivative @@ -351,7 +351,9 @@ def _call(self, ufunc): def sin(self): """Sine of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.sin) else: from sympy import sin as _sin @@ -360,7 +362,9 @@ def sin(self): def cos(self): """Cosine of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.cos) else: from sympy import cos as _cos @@ -369,7 +373,9 @@ def cos(self): def tan(self): """Tangent of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.tan) else: from sympy import tan as _tan @@ -378,7 +384,9 @@ def tan(self): def arcsin(self): """Arcsin of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.asin) else: from sympy import asin as _asin @@ -387,7 +395,9 @@ def arcsin(self): def arccos(self): """Arccos of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.acos) else: from sympy import acos as _acos @@ -396,7 +406,9 @@ def arccos(self): def arctan(self): """Arctan of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.atan) else: from sympy import atan as _atan @@ -405,7 +417,9 @@ def arctan(self): def exp(self): """Exponential of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.exp) else: from sympy import exp as _exp @@ -414,7 +428,9 @@ def exp(self): def log(self): """Logarithm of a ParameterExpression""" - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + return self._call(symengine.log) else: from sympy import log as _log @@ -480,7 +496,7 @@ def __eq__(self, other): if isinstance(other, ParameterExpression): if self.parameters != other.parameters: return False - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: from sympy import sympify return sympify(self._symbol_expr).equals(sympify(other._symbol_expr)) @@ -491,7 +507,7 @@ def __eq__(self, other): return False def __getstate__(self): - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: from sympy import sympify symbols = {k: sympify(v) for k, v in self._parameter_symbols.items()} @@ -507,6 +523,8 @@ def __getstate__(self): def __setstate__(self, state): if state["type"] == "symengine": + import 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) @@ -525,7 +543,7 @@ 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 HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: if self._symbol_expr.imag != 0.0: return False else: diff --git a/qiskit/circuit/qpy_serialization.py b/qiskit/circuit/qpy_serialization.py index dd4822c7d340..e6134050df93 100644 --- a/qiskit/circuit/qpy_serialization.py +++ b/qiskit/circuit/qpy_serialization.py @@ -649,13 +649,7 @@ class if it's defined in Qiskit. Otherwise it falls back to the custom from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators import SparsePauliOp from qiskit.synthesis import evolution as evo_synth - -try: - import symengine - - HAS_SYMENGINE = True -except ImportError: - HAS_SYMENGINE = False +from qiskit.utils import optionals as _optionals # v1 Binary Format @@ -917,7 +911,9 @@ def _read_parameter_expression_v3(file_obj, vectors): map_elements = param_expr_raw[0] from sympy.parsing.sympy_parser import parse_expr - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + expr = symengine.sympify(parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8"))) else: expr = parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8")) @@ -953,7 +949,9 @@ def _read_parameter_expression(file_obj): map_elements = param_expr_raw[0] from sympy.parsing.sympy_parser import parse_expr - if HAS_SYMENGINE: + if _optionals.HAS_SYMENGINE: + import symengine + expr = symengine.sympify(parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8"))) else: expr = parse_expr(file_obj.read(param_expr_raw[1]).decode("utf8")) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 065e0f74ff74..c9a41777bdab 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -35,7 +35,7 @@ from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.dagcircuit.exceptions import DAGCircuitError from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals class DAGCircuit: @@ -93,16 +93,11 @@ def __init__(self): self.duration = None self.unit = "dt" + @_optionals.HAS_NETWORKX.require_in_call def to_networkx(self): """Returns a copy of the DAGCircuit in networkx format.""" - try: - import networkx as nx - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="Networkx", - name="DAG converter to networkx", - pip_install="pip install networkx", - ) from ex + import networkx as nx + G = nx.MultiDiGraph() for node in self._multi_graph.nodes(): G.add_node(node) @@ -112,6 +107,7 @@ def to_networkx(self): return G @classmethod + @_optionals.HAS_NETWORKX.require_in_call def from_networkx(cls, graph): """Take a networkx MultiDigraph and create a new DAGCircuit. @@ -127,14 +123,8 @@ def from_networkx(cls, graph): MissingOptionalLibraryError: If networkx is not installed DAGCircuitError: If input networkx graph is malformed """ - try: - import networkx as nx - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="Networkx", - name="DAG converter from networkx", - pip_install="pip install networkx", - ) from ex + import networkx as nx + dag = DAGCircuit() for node in nx.topological_sort(graph): if isinstance(node, DAGOutNode): diff --git a/qiskit/extensions/hamiltonian_gate.py b/qiskit/extensions/hamiltonian_gate.py index c9a6ed84f91f..4106910c8acb 100644 --- a/qiskit/extensions/hamiltonian_gate.py +++ b/qiskit/extensions/hamiltonian_gate.py @@ -16,7 +16,6 @@ from numbers import Number import numpy -import scipy.linalg from qiskit.circuit import Gate, QuantumCircuit, QuantumRegister, ParameterExpression from qiskit.quantum_info.operators.predicates import matrix_equal @@ -80,6 +79,8 @@ def __eq__(self, other): def __array__(self, dtype=None): """Return matrix for the unitary.""" # pylint: disable=unused-argument + import scipy.linalg + try: return scipy.linalg.expm(-1j * self.params[0] * float(self.params[1])) except TypeError as ex: diff --git a/qiskit/opflow/gradients/gradient.py b/qiskit/opflow/gradients/gradient.py index bbe52600f809..dc2610ae1d23 100644 --- a/qiskit/opflow/gradients/gradient.py +++ b/qiskit/opflow/gradients/gradient.py @@ -17,8 +17,8 @@ import numpy as np from qiskit.circuit.quantumcircuit import _compare_parameters -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.circuit import ParameterExpression, ParameterVector +from qiskit.utils import optionals as _optionals from ..expectations.pauli_expectation import PauliExpectation from .gradient_base import GradientBase from .derivative_base import _coeff_derivative @@ -31,13 +31,6 @@ from ..state_fns.circuit_state_fn import CircuitStateFn from ..exceptions import OpflowError -try: - from jax import grad, jit - - _HAS_JAX = True -except ImportError: - _HAS_JAX = False - class Gradient(GradientBase): """Convert an operator expression to the first-order gradient.""" @@ -199,18 +192,10 @@ def is_coeff_c(coeff, c): if operator.grad_combo_fn: grad_combo_fn = operator.grad_combo_fn else: - if _HAS_JAX: - grad_combo_fn = jit(grad(operator.combo_fn, holomorphic=True)) - else: - raise MissingOptionalLibraryError( - libname="jax", - name="get_gradient", - msg=( - "This automatic differentiation function is based on JAX. " - "Please install jax and use `import jax.numpy as jnp` instead " - "of `import numpy as np` when defining a combo_fn." - ), - ) + _optionals.HAS_JAX.require_now("automatic differentiation") + from jax import jit, grad # pylint: disable=import-error + + grad_combo_fn = jit(grad(operator.combo_fn, holomorphic=True)) def chain_rule_combo_fn(x): result = np.dot(x[1], x[0]) diff --git a/qiskit/opflow/gradients/hessian.py b/qiskit/opflow/gradients/hessian.py index 20582af28c21..7cd32967214b 100644 --- a/qiskit/opflow/gradients/hessian.py +++ b/qiskit/opflow/gradients/hessian.py @@ -17,8 +17,8 @@ import numpy as np from qiskit.circuit.quantumcircuit import _compare_parameters -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.circuit import ParameterVector, ParameterExpression +from qiskit.utils import optionals as _optionals from ..operator_globals import Zero, One from ..state_fns.circuit_state_fn import CircuitStateFn from ..state_fns.state_fn import StateFn @@ -34,13 +34,6 @@ from ..exceptions import OpflowError from ...utils.arithmetic import triu_to_dense -try: - from jax import grad, jit - - _HAS_JAX = True -except ImportError: - _HAS_JAX = False - class Hessian(HessianBase): """Compute the Hessian of an expected value.""" @@ -242,38 +235,20 @@ def is_coeff_c(coeff, c): ] ) + _optionals.HAS_JAX.require_now("automatic differentiation") + from jax import grad, jit # pylint: disable=import-error + if operator.grad_combo_fn: first_partial_combo_fn = operator.grad_combo_fn - if _HAS_JAX: - second_partial_combo_fn = jit( - grad(lambda x: first_partial_combo_fn(x)[0], holomorphic=True) - ) - else: - raise MissingOptionalLibraryError( - libname="jax", - name="get_hessian", - msg=( - "This automatic differentiation function is based on JAX. Please " - "install jax and use `import jax.numpy as jnp` instead of " - "`import numpy as np` when defining a combo_fn." - ), - ) + + second_partial_combo_fn = jit( + grad(lambda x: first_partial_combo_fn(x)[0], holomorphic=True) + ) else: - if _HAS_JAX: - first_partial_combo_fn = jit(grad(operator.combo_fn, holomorphic=True)) - second_partial_combo_fn = jit( - grad(lambda x: first_partial_combo_fn(x)[0], holomorphic=True) - ) - else: - raise MissingOptionalLibraryError( - libname="jax", - name="get_hessian", - msg=( - "This automatic differentiation function is based on JAX. " - "Please install jax and use `import jax.numpy as jnp` instead " - "of `import numpy as np` when defining a combo_fn." - ), - ) + first_partial_combo_fn = jit(grad(operator.combo_fn, holomorphic=True)) + second_partial_combo_fn = jit( + grad(lambda x: first_partial_combo_fn(x)[0], holomorphic=True) + ) # For a general combo_fn F(g_0, g_1, ..., g_k) # dF/d θ0,θ1 = sum_i: (∂F/∂g_i)•(d g_i/ d θ0,θ1) + (∂F/∂^2 g_i)•(d g_i/d θ0)•(d g_i/d diff --git a/qiskit/opflow/gradients/natural_gradient.py b/qiskit/opflow/gradients/natural_gradient.py index c03ef76feb95..f1b814126c4f 100644 --- a/qiskit/opflow/gradients/natural_gradient.py +++ b/qiskit/opflow/gradients/natural_gradient.py @@ -19,7 +19,7 @@ from qiskit.circuit.quantumcircuit import _compare_parameters from qiskit.circuit import ParameterVector, ParameterExpression -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals from ..operator_base import OperatorBase from ..list_ops.list_op import ListOp from ..list_ops.composed_op import ComposedOp @@ -270,6 +270,7 @@ def get_lambda2_lambda3(lambda1, lambda4): return lambda_mc, x_mc @staticmethod + @_optionals.HAS_SKLEARN.require_in_call def _ridge( a: np.ndarray, c: np.ndarray, @@ -312,12 +313,7 @@ def _ridge( MissingOptionalLibraryError: scikit-learn not installed """ - try: - from sklearn.linear_model import Ridge - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="scikit-learn", name="_ridge", pip_install="pip install scikit-learn" - ) from ex + from sklearn.linear_model import Ridge reg = Ridge( alpha=lambda_, @@ -341,6 +337,7 @@ def reg_method(a, c, alpha): return lambda_mc, np.transpose(x_mc) @staticmethod + @_optionals.HAS_SKLEARN.require_in_call def _lasso( a: np.ndarray, c: np.ndarray, @@ -391,12 +388,7 @@ def _lasso( MissingOptionalLibraryError: scikit-learn not installed """ - try: - from sklearn.linear_model import Lasso - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="scikit-learn", name="_lasso", pip_install="pip install scikit-learn" - ) from ex + from sklearn.linear_model import Lasso reg = Lasso( alpha=lambda_, diff --git a/qiskit/quantum_info/operators/channel/transformations.py b/qiskit/quantum_info/operators/channel/transformations.py index 833b4a5fe1e9..4ded0ce11cc7 100644 --- a/qiskit/quantum_info/operators/channel/transformations.py +++ b/qiskit/quantum_info/operators/channel/transformations.py @@ -18,7 +18,6 @@ """ import numpy as np -import scipy.linalg as la from qiskit.exceptions import QiskitError from qiskit.quantum_info.operators.predicates import is_hermitian_matrix @@ -219,6 +218,8 @@ def _kraus_to_choi(data): def _choi_to_kraus(data, input_dim, output_dim, atol=ATOL_DEFAULT): """Transform Choi representation to Kraus representation.""" + from scipy import linalg as la + # Check if hermitian matrix if is_hermitian_matrix(data, atol=atol): # Get eigen-decomposition of Choi-matrix diff --git a/qiskit/quantum_info/operators/measures.py b/qiskit/quantum_info/operators/measures.py index 2ea64f38740f..24583edf9f0e 100644 --- a/qiskit/quantum_info/operators/measures.py +++ b/qiskit/quantum_info/operators/measures.py @@ -17,7 +17,6 @@ import logging import warnings import numpy as np -from scipy import sparse from qiskit.exceptions import QiskitError, MissingOptionalLibraryError from qiskit.circuit.gate import Gate @@ -27,13 +26,7 @@ from qiskit.quantum_info.operators.channel import Choi, SuperOp from qiskit.quantum_info.states.densitymatrix import DensityMatrix from qiskit.quantum_info.states.measures import state_fidelity - -try: - import cvxpy - - _HAS_CVX = True -except ImportError: - _HAS_CVX = False +from qiskit.utils import optionals as _optionals logger = logging.getLogger(__name__) @@ -277,7 +270,9 @@ def diamond_norm(choi, **kwargs): function. See the CVXPY documentation for information on available SDP solvers. """ - _cvxpy_check("`diamond_norm`") # Check CVXPY is installed + from scipy import sparse + + cvxpy = _cvxpy_check("`diamond_norm`") # Check CVXPY is installed choi = Choi(_input_formatter(choi, Choi, "diamond_norm", "choi")) @@ -341,10 +336,9 @@ def cvx_bmat(mat_r, mat_i): def _cvxpy_check(name): """Check that a supported CVXPY version is installed""" # Check if CVXPY package is installed - if not _HAS_CVX: - raise QiskitError( - f"CVXPY package is requried for {name}. Install with `pip install cvxpy` to use." - ) + _optionals.HAS_CVXPY.require_now(name) + import cvxpy # pylint: disable=import-error + # Check CVXPY version version = cvxpy.__version__ if version[0] != "1": @@ -353,6 +347,7 @@ def _cvxpy_check(name): "diamond_norm", msg=f"Incompatible CVXPY version {version} found.", ) + return cvxpy # pylint: disable=too-many-return-statements diff --git a/qiskit/quantum_info/states/measures.py b/qiskit/quantum_info/states/measures.py index 6e37788d1d00..94197617530a 100644 --- a/qiskit/quantum_info/states/measures.py +++ b/qiskit/quantum_info/states/measures.py @@ -14,7 +14,6 @@ """ import numpy as np -import scipy.linalg as la from qiskit.exceptions import QiskitError from qiskit.quantum_info.states.statevector import Statevector from qiskit.quantum_info.states.densitymatrix import DensityMatrix @@ -119,6 +118,8 @@ def entropy(state, base=2): Raises: QiskitError: if the input state is not a valid QuantumState. """ + import scipy.linalg as la + state = _format_state(state, validate=True) if isinstance(state, Statevector): return 0 @@ -195,6 +196,8 @@ def concurrence(state): QiskitError: if input is not a bipartite QuantumState. QiskitError: if density matrix input is not a 2-qubit state. """ + import scipy.linalg as la + # Concurrence computation requires the state to be valid state = _format_state(state, validate=True) if isinstance(state, Statevector): diff --git a/qiskit/quantum_info/states/utils.py b/qiskit/quantum_info/states/utils.py index 6094e4d4bd2f..08232eaa788a 100644 --- a/qiskit/quantum_info/states/utils.py +++ b/qiskit/quantum_info/states/utils.py @@ -15,7 +15,6 @@ """ import numpy as np -import scipy.linalg as la from qiskit.exceptions import QiskitError from qiskit.quantum_info.states.statevector import Statevector @@ -148,6 +147,8 @@ def _funm_svd(matrix, func): ndarray: funm (N, N) Value of the matrix function specified by func evaluated at `A`. """ + import scipy.linalg as la + unitary1, singular_values, unitary2 = la.svd(matrix) diag_func_singular = np.diag(func(singular_values)) return unitary1.dot(diag_func_singular).dot(unitary2) diff --git a/qiskit/quantum_info/synthesis/one_qubit_decompose.py b/qiskit/quantum_info/synthesis/one_qubit_decompose.py index 9d0849576753..cc212fc1f3c9 100644 --- a/qiskit/quantum_info/synthesis/one_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/one_qubit_decompose.py @@ -17,7 +17,6 @@ import math import cmath import numpy as np -import scipy.linalg as la from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister @@ -221,6 +220,8 @@ def angles_and_phase(self, unitary): @staticmethod def _params_zyz(mat): """Return the Euler angles and phase for the ZYZ basis.""" + import scipy.linalg as la + # We rescale the input matrix to be special unitary (det(U) = 1) # This ensures that the quaternion representation is real coeff = la.det(mat) ** (-0.5) diff --git a/qiskit/quantum_info/synthesis/quaternion.py b/qiskit/quantum_info/synthesis/quaternion.py index df58907f1b9e..734fd37ee635 100644 --- a/qiskit/quantum_info/synthesis/quaternion.py +++ b/qiskit/quantum_info/synthesis/quaternion.py @@ -15,7 +15,6 @@ """ import math import numpy as np -import scipy.linalg as la class Quaternion: @@ -47,6 +46,8 @@ def __mul__(self, r): def norm(self): """Norm of quaternion.""" + import scipy.linalg as la + return la.norm(self.data) def normalize(self, inplace=False): diff --git a/qiskit/quantum_info/synthesis/two_qubit_decompose.py b/qiskit/quantum_info/synthesis/two_qubit_decompose.py index 2d209ecf97ed..f887f26b62de 100644 --- a/qiskit/quantum_info/synthesis/two_qubit_decompose.py +++ b/qiskit/quantum_info/synthesis/two_qubit_decompose.py @@ -33,7 +33,6 @@ import logging import numpy as np -import scipy.linalg as la from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.quantumcircuit import QuantumCircuit, Gate @@ -145,6 +144,8 @@ def __new__(cls, unitary_matrix, *, fidelity=(1.0 - 1.0e-9)): The overall decomposition scheme is taken from Drury and Love, arXiv:0806.4015 [quant-ph]. """ + from scipy import linalg as la + pi = np.pi pi2 = np.pi / 2 pi4 = np.pi / 4 @@ -1402,4 +1403,47 @@ def num_basis_gates(self, unitary): return np.argmax([trace_to_fid(traces[i]) * self.basis_fidelity ** i for i in range(4)]) -two_qubit_cnot_decompose = TwoQubitBasisDecomposer(CXGate()) +# This weird duplicated lazy structure is for backwards compatibility; Qiskit has historically +# always made ``two_qubit_cnot_decompose`` available publicly immediately on import, but it's quite +# expensive to construct, and we want to defer the obejct's creation until it's actually used. We +# only need to pass through the public methods that take `self` as a parameter. Using `__getattr__` +# doesn't work because it is only called if the normal resolution methods fail. Using +# `__getattribute__` is too messy for a simple one-off use object. + + +class _LazyTwoQubitCXDecomposer(TwoQubitBasisDecomposer): + __slots__ = ("_inner",) + + def __init__(self): # pylint: disable=super-init-not-called + self._inner = None + + def _load(self): + if self._inner is None: + self._inner = TwoQubitBasisDecomposer(CXGate()) + + def __call__(self, *args, **kwargs): + self._load() + return self._inner(*args, **kwargs) + + def traces(self, target): + self._load() + return self._inner.traces(target) + + def decomp1(self, target): + self._load() + return self._inner.decomp1(target) + + def decomp2_supercontrolled(self, target): + self._load() + return self._inner.decomp2_supercontrolled(target) + + def decomp3_supercontrolled(self, target): + self._load() + return self._inner.decomp3_supercontrolled(target) + + def num_basis_gates(self, unitary): + self._load() + return self._inner.num_basis_gates(unitary) + + +two_qubit_cnot_decompose = _LazyTwoQubitCXDecomposer() diff --git a/qiskit/quantum_info/synthesis/weyl.py b/qiskit/quantum_info/synthesis/weyl.py index 36bce3d16cae..43ad88982256 100644 --- a/qiskit/quantum_info/synthesis/weyl.py +++ b/qiskit/quantum_info/synthesis/weyl.py @@ -15,7 +15,6 @@ """ import numpy as np -import scipy.linalg as la # "Magic" basis used for the Weyl decomposition. The basis and its adjoint are stored individually # unnormalized, but such that their matrix multiplication is still the identity. This is because @@ -55,6 +54,8 @@ def weyl_coordinates(U): Returns: np.ndarray: Array of the 3 Weyl coordinates. """ + import scipy.linalg as la + pi2 = np.pi / 2 pi4 = np.pi / 4 diff --git a/qiskit/synthesis/evolution/matrix_synthesis.py b/qiskit/synthesis/evolution/matrix_synthesis.py index 71b67c0d1e1f..3af9d04b6195 100644 --- a/qiskit/synthesis/evolution/matrix_synthesis.py +++ b/qiskit/synthesis/evolution/matrix_synthesis.py @@ -12,7 +12,6 @@ """Exact synthesis of operator evolution via (exponentially expensive) matrix exponentiation.""" -from scipy.linalg import expm from qiskit.circuit.quantumcircuit import QuantumCircuit from .evolution_synthesis import EvolutionSynthesis @@ -28,6 +27,8 @@ class MatrixExponential(EvolutionSynthesis): """ def synthesize(self, evolution): + from scipy.linalg import expm + # get operators and time to evolve operators = evolution.operator time = evolution.time diff --git a/qiskit/test/base.py b/qiskit/test/base.py index c89cb115403e..a7765afbc1d2 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -30,14 +30,7 @@ import unittest from unittest.util import safe_repr -try: - import fixtures - import testtools - - HAS_FIXTURES = True -except ImportError: - HAS_FIXTURES = False - +from qiskit.utils import optionals as _optionals from .decorators import enforce_subclasses_call from .utils import Path, setup_test_logging @@ -48,7 +41,8 @@ # If testtools is installed use that as a (mostly) drop in replacement for # unittest's TestCase. This will enable the fixtures used for capturing stdout # stderr, and pylogging to attach the output to stestr's result stream. -if HAS_FIXTURES: +if _optionals.HAS_TESTTOOLS: + import testtools # pylint: disable=import-error class BaseTestCase(testtools.TestCase): """Base test class.""" @@ -228,7 +222,10 @@ class FullQiskitTestCase(QiskitTestCase): If you derive directly from it, you may try and instantiate the class without satisfying its dependencies.""" + @_optionals.HAS_FIXTURES.require_in_call("output-capturing test cases") def setUp(self): + import fixtures + super().setUp() if os.environ.get("QISKIT_TEST_CAPTURE_STREAMS"): stdout = self.useFixture(fixtures.StringStream("stdout")).stream @@ -300,5 +297,5 @@ def valid_comparison(value): # Maintain naming backwards compatibility for downstream packages. BasicQiskitTestCase = QiskitTestCase -if HAS_FIXTURES: +if _optionals.HAS_TESTTOOLS and _optionals.HAS_FIXTURES: QiskitTestCase = FullQiskitTestCase diff --git a/qiskit/test/decorators.py b/qiskit/test/decorators.py index e1f8e6b9e1e5..a5639afe8102 100644 --- a/qiskit/test/decorators.py +++ b/qiskit/test/decorators.py @@ -13,8 +13,8 @@ """Decorator for using with Qiskit unit tests.""" +import collections.abc import functools -import inspect import os import socket import sys @@ -22,6 +22,7 @@ import unittest from warnings import warn +from qiskit.utils import wrap_method from .testing_options import get_test_options HAS_NET_CONNECTION = None @@ -204,81 +205,6 @@ def _wrapper(self, *args, **kwargs): return _wrapper -class _WrappedMethodCall: - """Method call with extra functionality before and after. This is returned by - :obj:`~_WrappedMethod` when accessed as an atrribute.""" - - def __init__(self, descriptor, obj, objtype): - self.descriptor = descriptor - self.obj = obj - self.objtype = objtype - - def __call__(self, *args, **kwargs): - if self.descriptor.isclassmethod: - ref = self.objtype - else: - # obj if we're being accessed as an instance method, or objtype if as a class method. - ref = self.obj if self.obj is not None else self.objtype - for before in self.descriptor.before: - before(ref, *args, **kwargs) - out = self.descriptor.method(ref, *args, **kwargs) - for after in self.descriptor.after: - after(ref, *args, **kwargs) - return out - - -class _WrappedMethod: - """Descriptor which calls its two arguments in succession, correctly handling instance- and - class-method calls. - - It is intended that this class will replace the attribute that ``inner`` previously was on a - class or instance. When accessed as that attribute, this descriptor will behave it is the same - function call, but with the ``function`` called after. - """ - - def __init__(self, cls, name, before=None, after=None): - # Find the actual definition of the method, not just the descriptor output from getattr. - for cls_ in inspect.getmro(cls): - try: - self.method = cls_.__dict__[name] - break - except KeyError: - pass - else: - raise ValueError(f"Method '{name}' is not defined for class '{cls.__class__.__name__}'") - before = (before,) if before is not None else () - after = (after,) if after is not None else () - if isinstance(self.method, type(self)): - self.isclassmethod = self.method.isclassmethod - self.before = before + self.method.before - self.after = self.method.after + after - self.method = self.method.method - else: - self.isclassmethod = False - self.before = before - self.after = after - if isinstance(self.method, classmethod): - self.method = self.method.__func__ - self.isclassmethod = True - - def __get__(self, obj, objtype=None): - # No functools.wraps because we're probably about to be bound to a different context. - return _WrappedMethodCall(self, obj, objtype) - - -def _wrap_method(cls, name, before=None, after=None): - """Wrap the functionality the instance- or class-method ``{cls}.{name}`` with ``before`` and - ``after``. - - This mutates ``cls``, replacing the attribute ``name`` with the new functionality. - - If either ``before`` or ``after`` are given, they should be callables with a compatible - signature to the method referred to. They will be called immediately before or after the method - as appropriate, and any return value will be ignored. - """ - setattr(cls, name, _WrappedMethod(cls, name, before, after)) - - def enforce_subclasses_call( methods: Union[str, Iterable[str]], attr: str = "_enforce_subclasses_call_cache" ) -> Callable[[Type], Type]: @@ -352,7 +278,7 @@ def wrap_subclass_methods(cls): # Only wrap methods who are directly defined in this class; if we're resolving to a method # higher up the food chain, then it will already have been wrapped. for name in set(cls.__dict__) & methods: - _wrap_method( + wrap_method( cls, name, before=clear_call_status(name), @@ -366,13 +292,38 @@ def decorator(cls): # Do the extra bits after the main body of __init__ so we can check we're not overwriting # anything, and after __init_subclass__ in case the decorated class wants to influence the # creation of the subclass's methods before we get to them. - _wrap_method(cls, "__init__", after=initialize_call_memory) + wrap_method(cls, "__init__", after=initialize_call_memory) for name in methods: - _wrap_method(cls, name, before=save_call_status(name)) - _wrap_method(cls, "__init_subclass__", after=wrap_subclass_methods) + wrap_method(cls, name, before=save_call_status(name)) + wrap_method(cls, "__init_subclass__", after=wrap_subclass_methods) return cls return decorator -TEST_OPTIONS = get_test_options() +class _TestOptions(collections.abc.Mapping): + """Lazy-loading view onto the test options retrieved from the environment.""" + + __slots__ = ("_options",) + + def __init__(self): + self._options = None + + def _load(self): + if self._options is None: + self._options = get_test_options() + + def __getitem__(self, key): + self._load() + return self._options[key] + + def __iter__(self): + self._load() + return iter(self._options) + + def __len__(self): + self._load() + return len(self._options) + + +TEST_OPTIONS = _TestOptions() diff --git a/qiskit/test/mock/fake_backend.py b/qiskit/test/mock/fake_backend.py index 35f2dab42ad6..b0e00ab7809c 100644 --- a/qiskit/test/mock/fake_backend.py +++ b/qiskit/test/mock/fake_backend.py @@ -25,14 +25,8 @@ from qiskit import pulse from qiskit.exceptions import QiskitError from qiskit.test.mock import fake_job - -try: - from qiskit.providers import aer - - HAS_AER = True -except ImportError: - HAS_AER = False - from qiskit.providers import basicaer +from qiskit.utils import optionals as _optionals +from qiskit.providers import basicaer class _Credentials: @@ -110,7 +104,9 @@ def properties(self): @classmethod def _default_options(cls): - if HAS_AER: + if _optionals.HAS_AER: + from qiskit.providers import aer + return aer.QasmSimulator._default_options() else: return basicaer.QasmSimulatorPy._default_options() @@ -134,7 +130,9 @@ def run(self, run_input, **kwargs): "Invalid input object %s, must be either a " "QuantumCircuit, Schedule, or a list of either" % circuits ) - if HAS_AER: + if _optionals.HAS_AER: + from qiskit.providers import aer + if pulse_job: from qiskit.providers.aer.pulse import PulseSystemModel @@ -224,7 +222,9 @@ def properties(self): def run(self, qobj): """Main job in simulator""" - if HAS_AER: + if _optionals.HAS_AER: + from qiskit.providers import aer + if qobj.type == "PULSE": from qiskit.providers.aer.pulse import PulseSystemModel diff --git a/qiskit/tools/jupyter/__init__.py b/qiskit/tools/jupyter/__init__.py index 8593c12b5491..c6bc1e0690dd 100644 --- a/qiskit/tools/jupyter/__init__.py +++ b/qiskit/tools/jupyter/__init__.py @@ -101,7 +101,7 @@ from IPython import get_ipython from qiskit.test.mock import FakeBackend -from qiskit.tools.visualization import HAS_MATPLOTLIB +from qiskit.utils import optionals as _optionals from .jupyter_magics import ProgressBarMagic, StatusMagic from .progressbar import HTMLProgressBar from .version_table import VersionTable @@ -109,18 +109,6 @@ from .monospace import MonospacedOutput from .job_watcher import JobWatcher, JobWatcherMagic -if HAS_MATPLOTLIB: - from .backend_overview import BackendOverview - from .backend_monitor import _backend_monitor - -try: - from qiskit.providers.ibmq.ibmqbackend import IBMQBackend - - HAS_IBMQ = True -except ImportError: - HAS_IBMQ = False - - _IP = get_ipython() if _IP is not None: _IP.register_magics(ProgressBarMagic) @@ -128,9 +116,15 @@ _IP.register_magics(MonospacedOutput) _IP.register_magics(Copyright) _IP.register_magics(JobWatcherMagic) - if HAS_MATPLOTLIB: + if _optionals.HAS_MATPLOTLIB: + from .backend_overview import BackendOverview + from .backend_monitor import _backend_monitor + _IP.register_magics(BackendOverview) - if HAS_IBMQ: + if _optionals.HAS_IBMQ: + # pylint: disable=import-error + from qiskit.providers.ibmq import IBMQBackend # pylint: disable=no-name-in-module + HTML_FORMATTER = _IP.display_formatter.formatters["text/html"] # Make _backend_monitor the html repr for IBM Q backends HTML_FORMATTER.for_type(IBMQBackend, _backend_monitor) diff --git a/qiskit/tools/jupyter/job_watcher.py b/qiskit/tools/jupyter/job_watcher.py index 49e4d175c5b2..27bb4a9d8a0c 100644 --- a/qiskit/tools/jupyter/job_watcher.py +++ b/qiskit/tools/jupyter/job_watcher.py @@ -15,29 +15,20 @@ from IPython.core.magic import line_magic, Magics, magics_class from qiskit.tools.events.pubsub import Subscriber -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals -try: - from qiskit.providers.ibmq.job.exceptions import IBMQJobApiError - - HAS_IBMQ = True -except ImportError: - HAS_IBMQ = False from .job_widgets import build_job_viewer, make_clear_button, make_labels, create_job_widget from .watcher_monitor import _job_monitor +@_optionals.HAS_IBMQ.require_in_instance class JobWatcher(Subscriber): """An IBM Q job watcher.""" + # pylint: disable=import-error + def __init__(self): super().__init__() - if not HAS_IBMQ: - raise MissingOptionalLibraryError( - libname="qiskit-ibmq-provider", - name="the job watcher", - pip_install="pip install qiskit-ibmq-provider", - ) self.jobs = [] self._init_subscriber() self.job_viewer = None @@ -107,6 +98,10 @@ def cancel_job(self, job_id): Raises: Exception: Job id not found. """ + from qiskit.providers.ibmq.job.exceptions import ( # pylint: disable=no-name-in-module + IBMQJobApiError, + ) + do_pop = False ind = None for idx, job in enumerate(self.jobs): @@ -169,6 +164,6 @@ def qiskit_disable_job_watcher(self, line="", cell=None): _JOB_WATCHER.stop_viewer() -if HAS_IBMQ: +if _optionals.HAS_IBMQ: # The Jupyter job watcher instance _JOB_WATCHER = JobWatcher() diff --git a/qiskit/tools/jupyter/jupyter_magics.py b/qiskit/tools/jupyter/jupyter_magics.py index b742e5d2a2d8..216e999162e1 100644 --- a/qiskit/tools/jupyter/jupyter_magics.py +++ b/qiskit/tools/jupyter/jupyter_magics.py @@ -19,18 +19,8 @@ from IPython.core import magic_arguments from IPython.core.magic import cell_magic, line_magic, Magics, magics_class, register_line_magic -from qiskit.exceptions import MissingOptionalLibraryError - -try: - import ipywidgets as widgets -except ImportError as ex: - raise MissingOptionalLibraryError( - libname="ipywidgets", - name="jupyter magics", - pip_install="pip install ipywidgets", - ) from ex +from qiskit.utils import optionals as _optionals import qiskit -from qiskit.visualization.matplotlib import HAS_MATPLOTLIB from qiskit.tools.events.progressbar import TextProgressBar from .progressbar import HTMLProgressBar from .library import circuit_library_widget @@ -81,8 +71,11 @@ class StatusMagic(Magics): @magic_arguments.argument( "-i", "--interval", type=float, default=None, help="Interval for status check." ) + @_optionals.HAS_IPYWIDGETS.require_in_call def qiskit_job_status(self, line="", cell=None): """A Jupyter magic function to check the status of a Qiskit job instance.""" + import ipywidgets as widgets + args = magic_arguments.parse_argstring(self.qiskit_job_status, line) if args.interval is None: @@ -178,7 +171,7 @@ def qiskit_progress_bar(self, line="", cell=None): # pylint: disable=unused-arg return pbar -if HAS_MATPLOTLIB and get_ipython(): +if _optionals.HAS_MATPLOTLIB and get_ipython(): @register_line_magic def circuit_library_info(circuit: qiskit.QuantumCircuit) -> None: diff --git a/qiskit/tools/jupyter/progressbar.py b/qiskit/tools/jupyter/progressbar.py index 0e8f7af9d062..3c57b0855681 100644 --- a/qiskit/tools/jupyter/progressbar.py +++ b/qiskit/tools/jupyter/progressbar.py @@ -47,18 +47,11 @@ import time -try: - import ipywidgets as widgets - - HAS_IPYWIDGETS = True -except ImportError: - HAS_IPYWIDGETS = False -from IPython.display import display - from qiskit.tools.events.progressbar import BaseProgressBar -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals +@_optionals.HAS_IPYWIDGETS.require_in_instance class HTMLProgressBar(BaseProgressBar): """ A simple HTML progress bar for using in IPython notebooks. @@ -69,12 +62,6 @@ def __init__(self): self.progress_bar = None self.label = None self.box = None - if not HAS_IPYWIDGETS: - raise MissingOptionalLibraryError( - libname="ipywidgets", - name="progress bar", - pip_install="pip install ipywidgets", - ) self._init_subscriber() def _init_subscriber(self): @@ -112,6 +99,9 @@ def _finish_progress_bar(): self.subscribe("terra.parallel.finish", _finish_progress_bar) def start(self, iterations): + import ipywidgets as widgets + from IPython.display import display + self.touched = True self.iter = int(iterations) self.t_start = time.time() diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 7324fe14abf1..4a3c3337fe5f 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -23,8 +23,6 @@ import warnings import numpy as np -import scipy.sparse as sp -import scipy.sparse.csgraph as cs import retworkx as rx from qiskit.transpiler.exceptions import CouplingError @@ -264,6 +262,8 @@ def reduce(self, mapping): Raises: CouplingError: Reduced coupling map must be connected. """ + from scipy.sparse import coo_matrix, csgraph + reduced_qubits = len(mapping) inv_map = [None] * (max(mapping) + 1) for idx, val in enumerate(mapping): @@ -280,9 +280,9 @@ def reduce(self, mapping): cols = np.array([edge[1] for edge in reduced_cmap], dtype=int) data = np.ones_like(rows) - mat = sp.coo_matrix((data, (rows, cols)), shape=(reduced_qubits, reduced_qubits)).tocsr() + mat = coo_matrix((data, (rows, cols)), shape=(reduced_qubits, reduced_qubits)).tocsr() - if cs.connected_components(mat)[0] != 1: + if csgraph.connected_components(mat)[0] != 1: raise CouplingError("coupling_map must be connected.") return CouplingMap(reduced_cmap) diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index f10b676eaea6..f45ef6c7b8fc 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -79,6 +79,7 @@ RemoveDiagonalGatesBeforeMeasure RemoveResetInZeroState CrosstalkAdaptiveSchedule + HoareOptimizer TemplateOptimization EchoRZXWeylDecomposition diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index 4024b1138aeb..cb60e1937de9 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -14,8 +14,6 @@ import numpy as np -import scipy.sparse as sp -import scipy.sparse.csgraph as cs from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import AnalysisPass @@ -61,6 +59,8 @@ def run(self, dag): Raises: TranspilerError: if dag wider than self.coupling_map """ + from scipy.sparse import coo_matrix + num_dag_qubits = sum(qreg.size for qreg in dag.qregs.values()) if num_dag_qubits > self.coupling_map.size(): raise TranspilerError("Number of qubits greater than device.") @@ -89,7 +89,7 @@ def run(self, dag): else: continue - self.cx_mat = sp.coo_matrix( + self.cx_mat = coo_matrix( (cx_err, (rows, cols)), shape=(device_qubits, device_qubits) ).tocsr() @@ -123,6 +123,8 @@ def _best_subset(self, num_qubits): Returns: ndarray: Array of qubits to use for best connectivity mapping. """ + from scipy.sparse import coo_matrix, csgraph + if num_qubits == 1: return np.array([0]) if num_qubits == 0: @@ -132,7 +134,7 @@ def _best_subset(self, num_qubits): cmap = np.asarray(self.coupling_map.get_edges()) data = np.ones_like(cmap[:, 0]) - sp_cmap = sp.coo_matrix( + sp_cmap = coo_matrix( (data, (cmap[:, 0], cmap[:, 1])), shape=(device_qubits, device_qubits) ).tocsr() best = 0 @@ -141,7 +143,7 @@ def _best_subset(self, num_qubits): best_sub = None # do bfs with each node as starting point for k in range(sp_cmap.shape[0]): - bfs = cs.breadth_first_order( + bfs = csgraph.breadth_first_order( sp_cmap, i_start=k, directed=False, return_predecessors=False ) @@ -189,7 +191,7 @@ def _best_subset(self, num_qubits): rows = [edge[0] for edge in new_cmap] cols = [edge[1] for edge in new_cmap] data = [1] * len(rows) - sp_sub_graph = sp.coo_matrix((data, (rows, cols)), shape=(num_qubits, num_qubits)).tocsr() - perm = cs.reverse_cuthill_mckee(sp_sub_graph) + sp_sub_graph = coo_matrix((data, (rows, cols)), shape=(num_qubits, num_qubits)).tocsr() + perm = csgraph.reverse_cuthill_mckee(sp_sub_graph) best_map = best_map[perm] return best_map diff --git a/qiskit/transpiler/passes/optimization/_gate_extension.py b/qiskit/transpiler/passes/optimization/_gate_extension.py index c4a57f294529..71e1d6f1f205 100644 --- a/qiskit/transpiler/passes/optimization/_gate_extension.py +++ b/qiskit/transpiler/passes/optimization/_gate_extension.py @@ -18,27 +18,24 @@ If a gate has no `_trivial_if`, then is assumed to be non-trivial. If a gate has no `_postconditions`, then is assumed to have unknown post-conditions. """ -try: - from z3 import Not, And - - HAS_Z3 = True -except ImportError: - HAS_Z3 = False from qiskit.circuit.library.standard_gates import IGate, XGate, YGate, ZGate from qiskit.circuit.library.standard_gates import CXGate, CCXGate, CYGate, CZGate from qiskit.circuit.library.standard_gates import TGate, TdgGate, SGate, SdgGate, RZGate, U1Gate from qiskit.circuit.library.standard_gates import SwapGate, CSwapGate, CRZGate, CU1Gate, MCU1Gate +from qiskit.utils import optionals as _optionals + +if _optionals.HAS_Z3: + import z3 # pylint: disable=import-error -if HAS_Z3: # FLIP GATES # # XGate - XGate._postconditions = lambda self, x1, y1: y1 == Not(x1) - CXGate._postconditions = lambda self, x1, y1: y1 == Not(x1) - CCXGate._postconditions = lambda self, x1, y1: y1 == Not(x1) + XGate._postconditions = lambda self, x1, y1: y1 == z3.Not(x1) + CXGate._postconditions = lambda self, x1, y1: y1 == z3.Not(x1) + CCXGate._postconditions = lambda self, x1, y1: y1 == z3.Not(x1) # YGate - YGate._postconditions = lambda self, x1, y1: y1 == Not(x1) - CYGate._postconditions = lambda self, x1, y1: y1 == Not(x1) + YGate._postconditions = lambda self, x1, y1: y1 == z3.Not(x1) + CYGate._postconditions = lambda self, x1, y1: y1 == z3.Not(x1) # PHASE GATES # # IdGate @@ -78,6 +75,6 @@ # MULTI-QUBIT GATES # # SwapGate SwapGate._trivial_if = lambda self, x1, x2: x1 == x2 - SwapGate._postconditions = lambda self, x1, x2, y1, y2: And(x1 == y2, x2 == y1) + SwapGate._postconditions = lambda self, x1, x2, y1, y2: z3.And(x1 == y2, x2 == y1) CSwapGate._trivial_if = lambda self, x1, x2: x1 == x2 - CSwapGate._postconditions = lambda self, x1, x2, y1, y2: And(x1 == y2, x2 == y1) + CSwapGate._postconditions = lambda self, x1, x2, y1, y2: z3.And(x1 == y2, x2 == y1) diff --git a/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py b/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py index 0f3c6f864279..dd4f75481ade 100644 --- a/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py +++ b/qiskit/transpiler/passes/optimization/crosstalk_adaptive_schedule.py @@ -32,28 +32,25 @@ import operator from itertools import chain, combinations -try: - from z3 import Real, Bool, Sum, Implies, And, Or, Not, Optimize - - HAS_Z3 = True -except ImportError: - HAS_Z3 = False from qiskit.transpiler.basepasses import TransformationPass from qiskit.dagcircuit import DAGCircuit from qiskit.circuit.library.standard_gates import U1Gate, U2Gate, U3Gate, CXGate from qiskit.circuit import Measure from qiskit.circuit.barrier import Barrier from qiskit.dagcircuit import DAGOpNode -from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils import optionals as _optionals NUM_PREC = 10 TWOQ_XTALK_THRESH = 3 ONEQ_XTALK_THRESH = 2 +@_optionals.HAS_Z3.require_in_instance class CrosstalkAdaptiveSchedule(TransformationPass): """Crosstalk mitigation through adaptive instruction scheduling.""" + # pylint: disable=import-error + def __init__(self, backend_prop, crosstalk_prop, weight_factor=0.5, measured_qubits=None): """CrosstalkAdaptiveSchedule initializer. @@ -94,6 +91,8 @@ def __init__(self, backend_prop, crosstalk_prop, weight_factor=0.5, measured_qub ImportError: if unable to import z3 solver """ + import z3 + super().__init__() self.backend_prop = backend_prop self.crosstalk_prop = crosstalk_prop @@ -121,7 +120,7 @@ def __init__(self, backend_prop, crosstalk_prop, weight_factor=0.5, measured_qub self.qubit_lifetime = {} self.dag_overlap_set = {} self.xtalk_overlap_set = {} - self.opt = Optimize() + self.opt = z3.Optimize() self.measured_qubits = [] self.measure_start = None self.last_gate_on_qubit = None @@ -279,13 +278,15 @@ def create_z3_vars(self): """ Setup the variables required for Z3 optimization """ + import z3 + for gate in self.dag.gate_nodes(): t_var_name = "t_" + str(self.gate_id[gate]) d_var_name = "d_" + str(self.gate_id[gate]) f_var_name = "f_" + str(self.gate_id[gate]) - self.gate_start_time[gate] = Real(t_var_name) - self.gate_duration[gate] = Real(d_var_name) - self.gate_fidelity[gate] = Real(f_var_name) + self.gate_start_time[gate] = z3.Real(t_var_name) + self.gate_duration[gate] = z3.Real(d_var_name) + self.gate_fidelity[gate] = z3.Real(f_var_name) for gate in self.xtalk_overlap_set: self.overlap_indicator[gate] = {} self.overlap_amounts[gate] = {} @@ -297,16 +298,16 @@ def create_z3_vars(self): else: # Indicator variable for overlap of g_1 and g_2 var_name1 = "olp_ind_" + str(self.gate_id[g_1]) + "_" + str(self.gate_id[g_2]) - self.overlap_indicator[g_1][g_2] = Bool(var_name1) + self.overlap_indicator[g_1][g_2] = z3.Bool(var_name1) var_name2 = "olp_amnt_" + str(self.gate_id[g_1]) + "_" + str(self.gate_id[g_2]) - self.overlap_amounts[g_1][g_2] = Real(var_name2) + self.overlap_amounts[g_1][g_2] = z3.Real(var_name2) active_qubits_list = [] for gate in self.dag.gate_nodes(): for q in gate.qargs: active_qubits_list.append(self.qubit_indices[q]) for active_qubit in list(set(active_qubits_list)): q_var_name = "l_" + str(active_qubit) - self.qubit_lifetime[active_qubit] = Real(q_var_name) + self.qubit_lifetime[active_qubit] = z3.Real(q_var_name) meas_q = [] for node in self.dag.op_nodes(): @@ -314,7 +315,7 @@ def create_z3_vars(self): meas_q.append(self.qubit_indices[node.qargs[0]]) self.measured_qubits = list(set(self.input_measured_qubits).union(set(meas_q))) - self.measure_start = Real("meas_start") + self.measure_start = z3.Real("meas_start") def basic_bounds(self): """ @@ -339,6 +340,8 @@ def scheduling_constraints(self): DAG scheduling constraints optimization Sets overlap indicator variables """ + import z3 + for gate in self.gate_start_time: for dep_gate in self.dag.successors(gate): if not isinstance(dep_gate, DAGOpNode): @@ -362,16 +365,18 @@ def scheduling_constraints(self): # This constraint enforces full or zero overlap between two gates before = f_1 < s_2 after = f_2 < s_1 - overlap1 = And(s_2 <= s_1, f_1 <= f_2) - overlap2 = And(s_1 <= s_2, f_2 <= f_1) - self.opt.add(Or(before, after, overlap1, overlap2)) - intervals_overlap = And(s_2 <= f_1, s_1 <= f_2) + overlap1 = z3.And(s_2 <= s_1, f_1 <= f_2) + overlap2 = z3.And(s_1 <= s_2, f_2 <= f_1) + self.opt.add(z3.Or(before, after, overlap1, overlap2)) + intervals_overlap = z3.And(s_2 <= f_1, s_1 <= f_2) self.opt.add(self.overlap_indicator[g_1][g_2] == intervals_overlap) def fidelity_constraints(self): """ Set gate fidelity based on gate overlap conditions """ + import z3 + for gate in self.gate_start_time: q_0 = self.qubit_indices[gate.qargs[0]] no_xtalk = False @@ -399,7 +404,7 @@ def fidelity_constraints(self): for tmpg in on_set: clauses.append(self.overlap_indicator[gate][tmpg]) for tmpg in off_set: - clauses.append(Not(self.overlap_indicator[gate][tmpg])) + clauses.append(z3.Not(self.overlap_indicator[gate][tmpg])) err = 0 if not on_set: err = self.bp_cx_err[self.cx_tuple(gate)] @@ -415,7 +420,7 @@ def fidelity_constraints(self): if err == 1.0: err = 0.999999 val = round(math.log(1.0 - err), NUM_PREC) - self.opt.add(Implies(And(*clauses), self.gate_fidelity[gate] == val)) + self.opt.add(z3.Implies(z3.And(*clauses), self.gate_fidelity[gate] == val)) def coherence_constraints(self): """ @@ -467,6 +472,8 @@ def objective_function(self): """ Objective function is a weighted combination of gate errors and decoherence errors """ + import z3 + self.fidelity_terms = [self.gate_fidelity[gate] for gate in self.gate_fidelity] self.coherence_terms = [] for q in self.qubit_lifetime: @@ -478,7 +485,7 @@ def objective_function(self): all_terms.append(self.weight_factor * item) for item in self.coherence_terms: all_terms.append((1 - self.weight_factor) * item) - self.opt.maximize(Sum(all_terms)) + self.opt.maximize(z3.Sum(all_terms)) def r2f(self, val): """ @@ -502,7 +509,9 @@ def solve_optimization(self): """ Setup and solve a Z3 optimization for finding the best schedule """ - self.opt = Optimize() + import z3 + + self.opt = z3.Optimize() self.create_z3_vars() self.basic_bounds() self.scheduling_constraints() @@ -702,12 +711,6 @@ def run(self, dag): """ Main scheduling function """ - if not HAS_Z3: - raise TranspilerError( - "z3-solver is required to use CrosstalkAdaptiveSchedule. " - 'To install, run "pip install z3-solver".' - ) - self.dag = dag # process input program diff --git a/qiskit/transpiler/passes/optimization/hoare_opt.py b/qiskit/transpiler/passes/optimization/hoare_opt.py index 85c4f4f8f696..68ccc6cc3c00 100644 --- a/qiskit/transpiler/passes/optimization/hoare_opt.py +++ b/qiskit/transpiler/passes/optimization/hoare_opt.py @@ -16,37 +16,30 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.extensions.unitary import UnitaryGate from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.transpiler.exceptions import TranspilerError from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.library.standard_gates import CZGate, CU1Gate, MCU1Gate -from . import _gate_extension # pylint: disable=unused-import - -try: - from z3 import And, Or, Not, Implies, Solver, Bool, unsat - - HAS_Z3 = True -except ImportError: - HAS_Z3 = False +from qiskit.utils import optionals as _optionals +@_optionals.HAS_Z3.require_in_instance class HoareOptimizer(TransformationPass): """This is a transpiler pass using Hoare logic circuit optimization. The inner workings of this are detailed in: https://arxiv.org/abs/1810.00375 """ + # pylint: disable=import-error + def __init__(self, size=10): """ Args: size (int): size of gate cache, in number of gates Raises: - TranspilerError: if unable to import z3 solver + MissingOptionalLibraryError: if unable to import z3 solver """ - if not HAS_Z3: - raise TranspilerError( - "z3-solver is required to use HoareOptimizer. " - 'To install, run "pip install z3-solver".' - ) + # This module is just a script that adds several post conditions onto existing classes. + from . import _gate_extension # pylint: disable=unused-import + super().__init__() self.solver = None self.variables = None @@ -64,8 +57,10 @@ def _gen_variable(self, qubit): Returns: BoolRef: z3 variable of qubit state """ + import z3 + varname = "q" + str(qubit) + "_" + str(self.gatenum[qubit]) - var = Bool(varname) + var = z3.Bool(varname) self.gatenum[qubit] += 1 self.variables[qubit].append(var) return var @@ -75,6 +70,7 @@ def _initialize(self, dag): Args: dag (DAGCircuit): input DAG to get qubits from """ + import z3 for qbt in dag.qubits: self.gatenum[qbt] = 0 @@ -82,7 +78,7 @@ def _initialize(self, dag): self.gatecache[qbt] = [] self.varnum[qbt] = {} x = self._gen_variable(qbt) - self.solver.add(Not(x)) + self.solver.add(z3.Not(x)) def _add_postconditions(self, gate, ctrl_ones, trgtqb, trgtvar): """create boolean variables for each qubit the gate is applied to @@ -96,17 +92,19 @@ def _add_postconditions(self, gate, ctrl_ones, trgtqb, trgtvar): trgtvar (list(BoolRef)): z3 variables corresponding to latest state of target qubits """ + import z3 + new_vars = [] for qbt in trgtqb: new_vars.append(self._gen_variable(qbt)) try: - self.solver.add(Implies(ctrl_ones, gate._postconditions(*(trgtvar + new_vars)))) + self.solver.add(z3.Implies(ctrl_ones, gate._postconditions(*(trgtvar + new_vars)))) except AttributeError: pass for i, tvar in enumerate(trgtvar): - self.solver.add(Implies(Not(ctrl_ones), new_vars[i] == tvar)) + self.solver.add(z3.Implies(z3.Not(ctrl_ones), new_vars[i] == tvar)) def _test_gate(self, gate, ctrl_ones, trgtvar): """use z3 sat solver to determine triviality of gate @@ -118,6 +116,8 @@ def _test_gate(self, gate, ctrl_ones, trgtvar): Returns: bool: if gate is trivial """ + import z3 + trivial = False self.solver.push() @@ -125,20 +125,20 @@ def _test_gate(self, gate, ctrl_ones, trgtvar): triv_cond = gate._trivial_if(*trgtvar) except AttributeError: self.solver.add(ctrl_ones) - trivial = self.solver.check() == unsat + trivial = self.solver.check() == z3.unsat else: if isinstance(triv_cond, bool): if triv_cond and len(trgtvar) == 1: - self.solver.add(Not(And(ctrl_ones, trgtvar[0]))) - sol1 = self.solver.check() == unsat + self.solver.add(z3.Not(z3.And(ctrl_ones, trgtvar[0]))) + sol1 = self.solver.check() == z3.unsat self.solver.pop() self.solver.push() - self.solver.add(And(ctrl_ones, trgtvar[0])) - sol2 = self.solver.check() == unsat + self.solver.add(z3.And(ctrl_ones, trgtvar[0])) + sol2 = self.solver.check() == z3.unsat trivial = sol1 or sol2 else: - self.solver.add(And(ctrl_ones, Not(triv_cond))) - trivial = self.solver.check() == unsat + self.solver.add(z3.And(ctrl_ones, z3.Not(triv_cond))) + trivial = self.solver.check() == z3.unsat self.solver.pop() return trivial @@ -183,11 +183,13 @@ def _remove_control(self, gate, ctrlvar, trgtvar): return remove, dag, qb def _check_removal(self, ctrlvar): - ctrl_ones = And(*ctrlvar) + import z3 + + ctrl_ones = z3.And(*ctrlvar) self.solver.push() - self.solver.add(Not(ctrl_ones)) - remove = self.solver.check() == unsat + self.solver.add(z3.Not(ctrl_ones)) + remove = self.solver.check() == z3.unsat self.solver.pop() return remove @@ -201,11 +203,13 @@ def _traverse_dag(self, dag): Args: dag (DAGCircuit): input DAG to optimize in place """ + import z3 + for node in dag.topological_op_nodes(): gate = node.op ctrlqb, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) - ctrl_ones = And(*ctrlvar) + ctrl_ones = z3.And(*ctrlvar) remove_ctrl, new_dag, qb_idx = self._remove_control(gate, ctrlvar, trgtvar) @@ -217,7 +221,7 @@ def _traverse_dag(self, dag): node.qargs = [(ctrlqb + trgtqb)[qi] for qi in qb_idx] _, ctrlvar, trgtqb, trgtvar = self._seperate_ctrl_trgt(node) - ctrl_ones = And(*ctrlvar) + ctrl_ones = z3.And(*ctrlvar) trivial = self._test_gate(gate, ctrl_ones, trgtvar) if trivial: @@ -313,6 +317,9 @@ def _seq_as_one(self, sequence): Returns: bool: if gate sequence is only executed completely or not at all """ + from z3 import Or, And, Not + import z3 + assert len(sequence) == 2 ctrlvar1 = self._seperate_ctrl_trgt(sequence[0])[1] ctrlvar2 = self._seperate_ctrl_trgt(sequence[1])[1] @@ -321,7 +328,7 @@ def _seq_as_one(self, sequence): self.solver.add( Or(And(And(*ctrlvar1), Not(And(*ctrlvar2))), And(Not(And(*ctrlvar1)), And(*ctrlvar2))) ) - res = self.solver.check() == unsat + res = self.solver.check() == z3.unsat self.solver.pop() return res @@ -387,7 +394,9 @@ def _reset(self): """Reset HoareOptimize internal state, so it can be run multiple times. """ - self.solver = Solver() + import z3 + + self.solver = z3.Solver() self.variables = {} self.gatenum = {} self.gatecache = {} diff --git a/qiskit/transpiler/passes/routing/algorithms/bip_model.py b/qiskit/transpiler/passes/routing/algorithms/bip_model.py index 20b6b07e7947..f0b7d576d4c3 100644 --- a/qiskit/transpiler/passes/routing/algorithms/bip_model.py +++ b/qiskit/transpiler/passes/routing/algorithms/bip_model.py @@ -16,7 +16,6 @@ import numpy as np -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.transpiler.exceptions import TranspilerError, CouplingError from qiskit.transpiler.layout import Layout from qiskit.circuit.library.standard_gates import SwapGate @@ -26,10 +25,12 @@ TwoQubitWeylDecomposition, trace_to_fid, ) +from qiskit.utils import optionals as _optionals logger = logging.getLogger(__name__) +@_optionals.HAS_DOCPLEX.require_in_instance class BIPMappingModel: """Internal model to create and solve a BIP problem for mapping. @@ -55,14 +56,6 @@ def __init__(self, dag, coupling_map, qubit_subset, dummy_timesteps=None): TranspilerError: If size of virtual qubits and physical qubits differ, or if coupling_map is not symmetric (bidirectional). """ - try: - from docplex.mp.model import Model # pylint: disable=unused-import - except ImportError as error: - raise MissingOptionalLibraryError( - libname="DOcplex", - name="Decision Optimization CPLEX Modeling for Python", - pip_install="pip install docplex", - ) from error self._dag = dag self._coupling = copy.deepcopy(coupling_map) # reduced coupling map @@ -154,6 +147,7 @@ def _is_dummy_step(self, t: int): """Check if the time-step t is a dummy step or not.""" return len(self.gates[t]) == 0 + @_optionals.HAS_DOCPLEX.require_in_call def create_cpx_problem( self, objective: str, @@ -197,14 +191,7 @@ def create_cpx_problem( self.default_cx_error_rate = default_cx_error_rate if self.bprop is None and self.default_cx_error_rate is None: raise TranspilerError("BackendProperties or default_cx_error_rate must be specified") - try: - from docplex.mp.model import Model - except ImportError as error: - raise MissingOptionalLibraryError( - libname="DOcplex", - name="Decision Optimization CPLEX Modeling for Python", - pip_install="pip install docplex", - ) from error + from docplex.mp.model import Model mdl = Model() @@ -438,6 +425,7 @@ def _mirrored_gate_fidelities(node): tracesm = two_qubit_cnot_decompose.traces(targetm) return [trace_to_fid(tracesm[i]) for i in range(4)] + @_optionals.HAS_CPLEX.require_in_call def solve_cpx_problem(self, time_limit: float = 60, threads: int = None) -> str: """Solve the BIP problem using CPLEX. @@ -454,14 +442,6 @@ def solve_cpx_problem(self, time_limit: float = 60, threads: int = None) -> str: Raises: MissingOptionalLibraryError: If CPLEX is not installed """ - try: - import cplex # pylint: disable=unused-import - except ImportError as error: - raise MissingOptionalLibraryError( - libname="CPLEX", - name="CplexOptimizer", - pip_install="pip install cplex", - ) from error self.problem.set_time_limit(time_limit) if threads is not None: self.problem.context.cplex_parameters.threads = threads diff --git a/qiskit/transpiler/passes/routing/bip_mapping.py b/qiskit/transpiler/passes/routing/bip_mapping.py index 696df2c9aa01..7a87b8272e40 100644 --- a/qiskit/transpiler/passes/routing/bip_mapping.py +++ b/qiskit/transpiler/passes/routing/bip_mapping.py @@ -18,7 +18,7 @@ from qiskit.circuit import QuantumRegister from qiskit.circuit.library.standard_gates import SwapGate from qiskit.dagcircuit import DAGCircuit, DAGOpNode -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals from qiskit.transpiler import TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.routing.algorithms.bip_model import BIPMappingModel @@ -26,6 +26,8 @@ logger = logging.getLogger(__name__) +@_optionals.HAS_CPLEX.require_in_instance("BIP-based mapping pass") +@_optionals.HAS_DOCPLEX.require_in_instance("BIP-based mapping pass") class BIPMapping(TransformationPass): r"""Map a DAGCircuit onto a given ``coupling_map``, allocating qubits and adding swap gates. @@ -109,16 +111,6 @@ def __init__( MissingOptionalLibraryError: if cplex or docplex are not installed. TranspilerError: if invalid options are specified. """ - try: - import docplex # pylint: disable=unused-import - import cplex # pylint: disable=unused-import - except ImportError as error: - raise MissingOptionalLibraryError( - libname="bip-mapper", - name="BIP-based mapping pass", - pip_install="pip install 'qiskit-terra[bip-mapper]'", - msg="This may not be possible for all Python versions and OSes", - ) from error super().__init__() self.coupling_map = coupling_map self.qubit_subset = qubit_subset diff --git a/qiskit/utils/__init__.py b/qiskit/utils/__init__.py index 3aa70917c1ac..f4461e5ba755 100644 --- a/qiskit/utils/__init__.py +++ b/qiskit/utils/__init__.py @@ -27,6 +27,7 @@ is_main_process apply_prefix detach_prefix + wrap_method Algorithm Utilities =================== @@ -55,6 +56,11 @@ are run on a device or simulator by passing a QuantumInstance setup with the desired backend etc. + +Optional Depedency Checkers (:mod:`qiskit.utils.optionals`) +=========================================================== + +.. automodule:: qiskit.utils.optionals """ from .quantum_instance import QuantumInstance @@ -63,6 +69,10 @@ from .multiprocessing import local_hardware_info from .multiprocessing import is_main_process from .units import apply_prefix, detach_prefix +from .classtools import wrap_method +from .lazy_tester import LazyDependencyManager, LazyImportTester, LazySubprocessTester + +from . import optionals from .circuit_utils import summarize_circuits from .entangler_map import get_entangler_map, validate_entangler_map @@ -72,6 +82,9 @@ __all__ = [ + "LazyDependencyManager", + "LazyImportTester", + "LazySubprocessTester", "QuantumInstance", "summarize_circuits", "get_entangler_map", diff --git a/qiskit/utils/classtools.py b/qiskit/utils/classtools.py new file mode 100644 index 000000000000..71c17754bdad --- /dev/null +++ b/qiskit/utils/classtools.py @@ -0,0 +1,161 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tools useful for creating decorators, and other high-level callables.""" + +import functools +import inspect +import types +from typing import Type, Callable + + +# On user-defined classes, `__new__` is magically inferred to be a staticmethod, `__init_subclass__` +# is magically inferred to be a class method and `__prepare__` must be defined as a classmethod, but +# the CPython types implemented in C (such as `object` and `type`) are `types.BuiltinMethodType`, +# which we can't distinguish properly, so we need a little magic. +_MAGIC_STATICMETHODS = {"__new__"} +_MAGIC_CLASSMETHODS = {"__init_subclass__", "__prepare__"} + +# `type` itself has several methods (mostly dunders). When we are wrapping those names, we need to +# make sure that we don't interfere with `type.__getattribute__`'s handling that circumvents the +# normal inheritance rules when appropriate. +_TYPE_METHODS = set(dir(type)) + + +class _lift_to_method: # pylint: disable=invalid-name + """A decorator that ensures that an input callable object implements ``__get__``. It is + returned unchanged if so, otherwise it is turned into the default implementation for functions, + which makes them bindable to instances. + + Python-space functions and lambdas already have this behaviour, but builtins like ``print`` + don't; using this class allows us to do:: + + wrap_method(MyClass, "maybe_mutates_arguments", before=print, after=print) + + to simply print all the arguments on entry and exit of the function, which otherwise wouldn't be + valid, since ``print`` isn't a descriptor. + """ + + __slots__ = ("_method",) + + def __new__(cls, method): + if hasattr(method, "__get__"): + return method + return super().__new__(cls) + + def __init__(self, method): + if method is self: + # Prevent double-initialisation if we are passed an instance of this object to lift. + return + self._method = method + + def __get__(self, obj, objtype): + # This is effectively the same implementation as `types.FunctionType.__get__`, but we can't + # bind that directly because it also includes a requirement that its `self` reference is of + # the correct type, and this isn't. + if obj is None: + return self._method + return types.MethodType(self._method, obj) + + +class _WrappedMethod: + """Descriptor which calls its two arguments in succession, correctly handling instance- and + class-method calls. + + It is intended that this class will replace the attribute that ``inner`` previously was on a + class or instance. When accessed as that attribute, this descriptor will behave it is the same + function call, but with the ``function`` called before or after. + """ + + __slots__ = ("_method_decorator", "_method_has_get", "_method", "_before", "_after") + + def __init__(self, method, before=None, after=None): + if isinstance(method, (classmethod, staticmethod)): + self._method_decorator = type(method) + elif isinstance(method, type(self)): + self._method_decorator = method._method_decorator + elif getattr(method, "__name__", None) in _MAGIC_STATICMETHODS: + self._method_decorator = staticmethod + elif getattr(method, "__name__", None) in _MAGIC_CLASSMETHODS: + self._method_decorator = classmethod + else: + self._method_decorator = _lift_to_method + before = (self._method_decorator(before),) if before is not None else () + after = (self._method_decorator(after),) if after is not None else () + if isinstance(method, type(self)): + self._method = method._method + self._before = before + method._before + self._after = method._after + after + else: + self._before = before + self._after = after + self._method = method + # If the inner method doesn't have `__get__` (like some builtin methods), it's faster to + # test a Boolean each time than the repeatedly raise and catch an exception, which is what + # `hasattr` does. + self._method_has_get = hasattr(self._method, "__get__") + + def __get__(self, obj, objtype=None): + # `self._method` doesn't invoke the `_method` descriptor (if it is one) because that only + # happens for class variables. Here it's an instance variable, so we can pass through `obj` + # and `objtype` correctly like this. + method = self._method.__get__(obj, objtype) if self._method_has_get else self._method + + @functools.wraps(method) + def out(*args, **kwargs): + for callback in self._before: + callback.__get__(obj, objtype)(*args, **kwargs) + retval = method(*args, **kwargs) + for callback in self._after: + callback.__get__(obj, objtype)(*args, **kwargs) + return retval + + return out + + +def wrap_method(cls: Type, name: str, *, before: Callable = None, after: Callable = None): + """Wrap the functionality the instance- or class method ``cls.name`` with additional behaviour + ``before`` and ``after``. + + This mutates ``cls``, replacing the attribute ``name`` with the new functionality. This is + useful when creating class decorators. The method is allowed to be defined on any parent class + instead. + + If either ``before`` or ``after`` are given, they should be callables with a compatible + signature to the method referred to. They will be called immediately before or after the method + as appropriate, and any return value will be ignored. + + Args: + cls: the class to modify. + name: the name of the method on the class to wrap. + before: a callable that should be called before the method that is being wrapped. + after: a callable that should be called after the method that is being wrapped. + + Raises: + ValueError: if the named method is not defined on the class or any parent class. + """ + # The best time to apply decorators to methods is before they are bound (e.g. by using function + # decorators during the class definition), but if we're making a class decorator, we can't do + # that. We need the actual definition of the method, so we have to dodge the normal output of + # `type.__getattribute__`, which evalutes descriptors if it finds them, unless the name we're + # looking for is defined on `type` itself. In that case, we need the attribute getter to + # correctly return the underlying object, not the one that `type` defines for its own purposes. + attribute_getter = type.__getattribute__ if name in _TYPE_METHODS else object.__getattribute__ + for cls_ in inspect.getmro(cls): + try: + method = attribute_getter(cls_, name) + break + except AttributeError: + pass + else: + raise ValueError(f"Method '{name}' is not defined for class '{cls.__name__}'") + setattr(cls, name, _WrappedMethod(method, before, after)) diff --git a/qiskit/utils/lazy_tester.py b/qiskit/utils/lazy_tester.py new file mode 100644 index 000000000000..4f0d8e5a37aa --- /dev/null +++ b/qiskit/utils/lazy_tester.py @@ -0,0 +1,334 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Lazy testers for optional features.""" + +import abc +import contextlib +import functools +import importlib +import subprocess +import typing +from typing import Union, Iterable, Dict, Optional, Callable, Type + +from qiskit.exceptions import MissingOptionalLibraryError +from .classtools import wrap_method + + +class _RequireNow: + """Helper callable that accepts all function signatures and simply calls + :meth:`.LazyDependencyManager.require_now`. This helpful when used with :func:`.wrap_method`, + as the callable needs to be compatible with all signatures and be picklable.""" + + __slots__ = ("_tester", "_feature") + + def __init__(self, tester, feature): + self._tester = tester + self._feature = feature + + def __call__(self, *_args, **_kwargs): + self._tester.require_now(self._feature) + + +class LazyDependencyManager(abc.ABC): + """A mananger for some optional features that are expensive to import, or to verify the + existence of. + + These objects can be used as Booleans, such as ``if x``, and will evaluate ``True`` if the + dependency they test for is available, and ``False`` if not. The presence of the dependency + will only be tested when the Boolean is evaluated, so it can be used as a runtime test in + functions and methods without requiring an import-time test. + + These objects also encapsulate the error handling if their dependency is not present, so you can + do things such as:: + + from qiskit.utils import LazyImportManager + HAS_MATPLOTLIB = LazyImportManager("matplotlib") + + @HAS_MATPLOTLIB.require_in_call + def my_visualisation(): + ... + + def my_other_visualisation(): + # ... some setup ... + HAS_MATPLOTLIB.require_now("my_other_visualisation") + ... + + def my_third_visualisation(): + if HAS_MATPLOTLIB: + from matplotlib import pyplot + else: + ... + + In all of these cases, ``matplotlib`` is not imported until the functions are entered. In the + case of the decorator, ``matplotlib`` is tested for import when the function is called for + the first time. In the second and third cases, the loader attempts to import ``matplotlib`` + when the :meth:`require_now` method is called, or when the Boolean context is evaluated. For + the ``require`` methods, an error is raised if the library is not available. + + This is the base class, which provides the Boolean context checking and error management. The + concrete classes :class:`LazyImportTester` and :class:`LazySubprocessTester` provide convenient + entry points for testing that certain symbols are importable from modules, or certain + command-line tools are available, respectively. + """ + + __slots__ = ("_bool", "_callback", "_name", "_install", "_msg") + + def __init__(self, *, name=None, callback=None, install=None, msg=None): + """ + Args: + name: the name of this optional dependency. + callback: a callback that is called immediately after the availability of the library is + tested with the result. This will only be called once. + install: how to install this optional dependency. Passed to + :class:`.MissingOptionalLibraryError` as the ``pip_install`` parameter. + msg: an extra message to include in the error raised if this is required. + """ + self._bool = None + self._callback = callback + self._name = name + self._install = install + self._msg = msg + + @abc.abstractmethod + def _is_available(self) -> bool: + """Subclasses of :class:`LazyDependencyManager` should override this method to implement the + actual test of availability. This method should return a Boolean, where ``True`` indicates + that the dependency was available. This method will only ever be called once. + + :meta public: + """ + return False + + def __bool__(self): + if self._bool is None: + self._bool = self._is_available() + if self._callback is not None: + self._callback(self._bool) + return self._bool + + @typing.overload + def require_in_call(self, feature_or_callable: Callable) -> Callable: + ... + + @typing.overload + def require_in_call(self, feature_or_callable: str) -> Callable[[Callable], Callable]: + ... + + def require_in_call(self, feature_or_callable): + """Create a decorator for callables that requires that the dependency is available when the + decorated function or method is called. + + Args: + feature_or_callable (str or Callable): the name of the feature that requires these + dependencies. If this function is called directly as a decorator (for example + ``@HAS_X.require_in_call`` as opposed to + ``@HAS_X.require_in_call("my feature")``), then the feature name will be taken to be + the function name, or class and method name as appropriate. + + Returns: + Callable: a decorator that will make its argument require this dependency before it is + called. + """ + if isinstance(feature_or_callable, str): + feature = feature_or_callable + + def decorator(function): + @functools.wraps(function) + def out(*args, **kwargs): + self.require_now(feature) + return function(*args, **kwargs) + + return out + + return decorator + + function = feature_or_callable + feature = ( + getattr(function, "__qualname__", None) + or getattr(function, "__name__", None) + or str(function) + ) + + @functools.wraps(function) + def out(*args, **kwargs): + self.require_now(feature) + return function(*args, **kwargs) + + return out + + @typing.overload + def require_in_instance(self, feature_or_class: Type) -> Type: + ... + + @typing.overload + def require_in_instance(self, feature_or_class: str) -> Callable[[Type], Type]: + ... + + def require_in_instance(self, feature_or_class): + """A class decorator that requires the dependency is available when the class is + initialised. This decorator can be used even if the class does not define an ``__init__`` + method. + + Args: + feature_or_class (str or Type): the name of the feature that requires these + dependencies. If this function is called directly as a decorator (for example + ``@HAS_X.require_in_instance`` as opposed to + ``@HAS_X.require_in_instance("my feature")``), then the feature name will be taken + as the name of the class. + + Returns: + Callable: a class decorator that ensures that the wrapped feature is present if the + class is initialised. + """ + if isinstance(feature_or_class, str): + feature = feature_or_class + + def decorator(class_): + wrap_method(class_, "__init__", before=_RequireNow(self, feature)) + return class_ + + return decorator + + class_ = feature_or_class + feature = ( + getattr(class_, "__qualname__", None) + or getattr(class_, "__name__", None) + or str(class_) + ) + wrap_method(class_, "__init__", before=_RequireNow(self, feature)) + return class_ + + def require_now(self, feature: str): + """Eagerly attempt to import the dependencies in this object, and raise an exception if they + cannot be imported. + + Args: + feature: the name of the feature that is requiring these dependencies. + + Raises: + MissingOptionalLibraryError: if the dependencies cannot be imported. + """ + if self: + return + raise MissingOptionalLibraryError( + libname=self._name, name=feature, pip_install=self._install, msg=self._msg + ) + + @contextlib.contextmanager + def disable_locally(self): + """ + Create a context, during which the value of the dependency manager will be ``False``. This + means that within the context, any calls to this object will behave as if the dependency is + not available, including raising errors. It is valid to call this method whether or not the + dependency has already been evaluated. This is most useful in tests. + """ + previous = self._bool + self._bool = False + try: + yield + finally: + self._bool = previous + + +class LazyImportTester(LazyDependencyManager): + """A lazy dependency tester for importable Python modules. Any required objects will only be + imported at the point that this object is tested for its Boolean value.""" + + __slots__ = ("_modules",) + + def __init__( + self, + name_map_or_modules: Union[str, Dict[str, Iterable[str]], Iterable[str]], + *, + name: Optional[str] = None, + callback: Optional[Callable[[bool], None]] = None, + install: Optional[str] = None, + msg: Optional[str] = None, + ): + """ + Args: + name_map_or_modules: if a name map, then a dictionary where the keys are modules or + packages, and the values are iterables of names to try and import from that + module. It should be valid to write ``from import , , ...``. + If simply a string or iterable of strings, then it should be valid to write + ``import `` for each of them. + + Raises: + ValueError: if no modules are given. + """ + if isinstance(name_map_or_modules, dict): + self._modules = {module: tuple(names) for module, names in name_map_or_modules.items()} + elif isinstance(name_map_or_modules, str): + self._modules = {name_map_or_modules: ()} + else: + self._modules = {module: () for module in name_map_or_modules} + if not self._modules: + raise ValueError("no modules supplied") + if name is not None: + pass + elif len(self._modules) == 1: + (name,) = self._modules.keys() + else: + all_names = tuple(self._modules.keys()) + name = f"{', '.join(all_names[:-1])} and {all_names[-1]}" + super().__init__(name=name, callback=callback, install=install, msg=msg) + + def _is_available(self): + try: + for module, names in self._modules.items(): + imported = importlib.import_module(module) + for name in names: + getattr(imported, name) + except (ImportError, AttributeError): + return False + return True + + +class LazySubprocessTester(LazyDependencyManager): + """A lazy checker that a command-line tool is available. The command will only be run once, at + the point that this object is checked for its Boolean value. + """ + + __slots__ = ("_command",) + + def __init__( + self, + command: Union[str, Iterable[str]], + *, + name: Optional[str] = None, + callback: Optional[Callable[[bool], None]] = None, + install: Optional[str] = None, + msg: Optional[str] = None, + ): + """ + Args: + command: the strings that make up the command to be run. For example, + ``["pdflatex", "-version"]``. + + Raises: + ValueError: if an empty command is given. + """ + self._command = (command,) if isinstance(command, str) else tuple(command) + if not self._command: + raise ValueError("no command supplied") + super().__init__(name=name or self._command[0], callback=callback, install=install, msg=msg) + + def _is_available(self): + try: + subprocess.run( + self._command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + except (OSError, subprocess.SubprocessError): + return False + else: + return True diff --git a/qiskit/utils/mitigation/_filters.py b/qiskit/utils/mitigation/_filters.py index c5ae2f7ecb31..6e7c467e874f 100644 --- a/qiskit/utils/mitigation/_filters.py +++ b/qiskit/utils/mitigation/_filters.py @@ -26,8 +26,6 @@ from copy import deepcopy import numpy as np -from scipy.optimize import minimize -import scipy.linalg as la import qiskit from qiskit import QiskitError @@ -106,6 +104,8 @@ def apply(self, raw_data, method="least_squares"): of the number of calibrated states. """ + from scipy.optimize import minimize + from scipy import linalg as la # check forms of raw_data if isinstance(raw_data, dict): @@ -352,6 +352,8 @@ def apply( Raises: QiskitError: if raw_data is not in a one of the defined forms. """ + from scipy.optimize import minimize + from scipy import linalg as la all_states = count_keys(self.nqubits) num_of_states = 2 ** self.nqubits diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py new file mode 100644 index 000000000000..3cd7043c51c4 --- /dev/null +++ b/qiskit/utils/optionals.py @@ -0,0 +1,294 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +.. currentmodule:: qiskit.utils.optionals + +Qiskit Terra, and many of the other Qiskit components, have several features that are enabled only +if certain *optional* dependencies are satisfied. This module is a collection of objects that can +be used to test if certain functionality is available, and optionally raise +:class:`.MissingOptionalLibraryError` if the functionality is not available. + + +Available Testers +================= + +Qiskit Components +----------------- + +.. list-table:: + :widths: 25 75 + + * - .. py:data:: HAS_AER + - :mod:`Qiskit Aer ` provides high-performance simulators for the + quantum circuits constructed within Qiskit Terra. + + * - .. py:data:: HAS_IBMQ + - The :mod:`Qiskit IBMQ Provider ` is used for accessing IBM Quantum + hardware in the IBM cloud. + + * - .. py:data:: HAS_IGNIS + - :mod:`Qiskit Ignis ` provides tools for quantum hardware verification, noise + characterization, and error correction. + + +External Python Libraries +------------------------- + +.. list-table:: + :widths: 25 75 + + * - .. py:data:: HAS_CPLEX + - The `IBM CPLEX Optimizer `__ is a + high-performance mathematical programming solver for linear, mixed-integer and quadratic + programming. It is required by the :class:`.BIPMapping` transpiler pass. + + * - .. py:data:: HAS_CVXPY + - `CVXPY `__ is a Python package for solving convex optimization + problems. It is required for calculating diamond norms with + :func:`.quantum_info.diamond_norm`. + + * - .. py:data:: HAS_DOCPLEX + - `IBM Decision Optimization CPLEX Modelling + `__ is a library for prescriptive + analysis. Like CPLEX, it is required for the :class:`.BIPMapping` transpiler pass. + + * - .. py:data:: HAS_FIXTURES + - The test suite has additional features that are available if the optional `fixtures + `__ module is installed. This generally also needs + :data:`HAS_TESTTOOLS` as well. This is generally only needed for Qiskit developers. + + * - .. py:data:: HAS_IPYTHON + - If `the IPython kernel `__ is available, certain additional + visualisations and line magics are made available. + + * - .. py:data:: HAS_IPYWIDGETS + - Monitoring widgets for jobs running on external backends can be provided if `ipywidgets + `__ is available. + + * - .. py:data:: HAS_JAX + - Some methods of gradient calculation within :mod:`.opflow.gradients` require `JAX + `__ for autodifferentiation. + + * - .. py:data:: HAS_MATPLOTLIB + - Qiskit Terra provides several visualisation tools in the :mod:`.visualization` module. + Almost all of these are built using `Matplotlib `__, which must + be installed in order to use them. + + * - .. py:data:: HAS_NETWORKX + - Internally, Qiskit uses the high-performance `retworkx + `__ library as a core dependency, but sometimes it can + be convenient to convert things into the Python-only `NetworkX `__ + format. There are converter methods on :class:`.DAGCircuit` if NetworkX is present. + + * - .. py:data:: HAS_NLOPT + - `NLOpt `__ is a nonlinear optimisation library, + used by the global optimizers in :mod:`.algorithms.optimizers`. See installation details in + :ref:`installing-nlopt`. + + * - .. py:data:: HAS_PIL + - PIL is a Python image-manipulation library. Qiskit actually uses the `pillow + `__ fork of PIL if it is available when generating + certain visualizations, for example of both :class:`.QuantumCircuit` and + :class:`.DAGCircuit` in certain modes. + + * - .. py:data:: HAS_PYDOT + - For some graph visualisations, Qiskit uses `pydot `__ as an + interface to GraphViz (see :data:`HAS_GRAPHVIZ`). + + * - .. py:data:: HAS_PYLATEX + - Various LaTeX-based visualizations, especially the circuit drawers, need access to the + `pylatexenc `__ project to work correctly. + + * - .. py:data:: HAS_SEABORN + - Qiskit Terra provides several visualisation tools in the :mod:`.visualization` module. Some + of these are built using `Seaborn `__, which must be installed + in order to use them. + + * - .. py:data:: HAS_SKLEARN + - Some of the gradient functions in :mod:`.opflow.gradients` use regularisation methods from + `Scikit Learn `__. + + * - .. py:data:: HAS_SKQUANT + - Some of the optimisers in :mod:`.algorithms.optimizers` are based on those found in `Scikit + Quant `__, which must be installed to use + them. + + * - .. py:data:: HAS_SQSNOBFIT + - `SQSnobFit `__ is a library for the "stable noisy + optimization by branch and fit" algorithm. It is used by the :class:`.SNOBFIT` optimizer. + + * - .. py:data:: HAS_SYMENGINE + - `Symengine `__ is a fast C++ backend for the + symbolic-manipulation library `Sympy `__. Qiskit uses + special methods from Symengine to accelerate its handling of + :class:`~.circuit.Parameter`\\ s if available. + + * - .. py:data:: HAS_TESTTOOLS + - Qiskit Terra's test suite has more advanced functionality available if the optional + `testtools `__ library is installed. This is generally + only needed for Qiskit developers. + + * - .. py:data:: HAS_Z3 + - `Z3 `__ is a theorem prover, used in the + :class:`.CrosstalkAdaptiveSchedule` and :class:`.HoareOptimizer` transpiler passes. + + +External Command-Line Tools +--------------------------- + +.. list-table:: + :widths: 25 75 + + * - .. py:data:: HAS_GRAPHVIZ + - For some graph visualisations, Qiskit uses the `GraphViz `__ + visualisation tool via its ``pydot`` interface (see :data:`HAS_PYDOT`). + + * - .. py:data:: HAS_PDFLATEX + - Visualisation tools that use LaTeX in their output, such as the circuit drawers, require + ``pdflatex`` to be available. You will generally need to ensure that you have a working + LaTeX installation available, and the ``qcircuit.tex`` package. + + * - .. py:data:: HAS_PDFTOCAIRO + - Visualisation tools that convert LaTeX-generated files into rasterised images use the + ``pdftocairo`` tool. This is part of the `Poppler suite of PDF tools + `__. + + +Lazy Checker Classes +==================== + +.. currentmodule:: qiskit.utils + +Each of the lazy checkers is an instance of :class:`.LazyDependencyManager` in one of its two +subclasses: :class:`.LazyImportTester` and :class:`.LazySubprocessTester`. These should be imported +from :mod:`.utils` directly if required, such as:: + + from qiskit.utils import LazyImportTester + +.. autoclass:: qiskit.utils.LazyDependencyManager + :members: + +.. autoclass:: qiskit.utils.LazyImportTester +.. autoclass:: qiskit.utils.LazySubprocessTester +""" + +import logging as _logging + +from .lazy_tester import ( + LazyImportTester as _LazyImportTester, + LazySubprocessTester as _LazySubprocessTester, +) + +_logger = _logging.getLogger(__name__) + +HAS_AER = _LazyImportTester( + "qiskit.providers.aer", + name="Qiskit Aer", + install="pip install qiskit-aer", +) +HAS_IBMQ = _LazyImportTester( + "qiskit.providers.ibmq", + name="IBMQ Provider", + install="pip install qiskit-ibmq-provider", +) +HAS_IGNIS = _LazyImportTester( + "qiskit.ignis", + name="Qiskit Ignis", + install="pip install qiskit-ignis", +) + +HAS_CPLEX = _LazyImportTester( + "cplex", + install="pip install 'qiskit-terra[bip-mapper]'", + msg="This may not be possible for all Python versions and OSes", +) +HAS_CVXPY = _LazyImportTester("cvxpy", install="pip install cvxpy") +HAS_DOCPLEX = _LazyImportTester( + {"docplex": (), "docplex.mp.model": ("Model",)}, + install="pip install 'qiskit-terra[bip-mapper]'", + msg="This may not be possible for all Python versions and OSes", +) +HAS_FIXTURES = _LazyImportTester("fixtures", install="pip install fixtures") +HAS_IPYTHON = _LazyImportTester("IPython", install="pip install ipython") +HAS_IPYWIDGETS = _LazyImportTester("ipywidgets", install="pip install ipywidgets") +HAS_JAX = _LazyImportTester( + {"jax": ("grad", "jit"), "jax.numpy": ()}, + name="jax", + install="pip install jax", +) +HAS_MATPLOTLIB = _LazyImportTester( + ("matplotlib.patches", "matplotlib.pyplot"), + name="matplotlib", + install="pip install matplotlib", +) +HAS_NETWORKX = _LazyImportTester("networkx", install="pip install networkx") + + +def _nlopt_callback(available): + if not available: + return + import nlopt # pylint: disable=import-error + + _logger.info( + "NLopt version: %s.%s.%s", + nlopt.version_major(), + nlopt.version_minor(), + nlopt.version_bugfix(), + ) + + +HAS_NLOPT = _LazyImportTester( + "nlopt", + name="NLopt Optimizer", + callback=_nlopt_callback, + msg="See the documentation of 'qiskit.algorithms.optimizer.nlopts' for installation help", +) +HAS_PIL = _LazyImportTester({"PIL": ("Image",)}, name="pillow", install="pip install pillow") +HAS_PYDOT = _LazyImportTester("pydot", install="pip install pydot") +HAS_PYLATEX = _LazyImportTester( + { + "pylatexenc.latex2text": ("LatexNodes2Text",), + "pylatexenc.latexencode": ("utf8tolatex",), + }, + name="pylatexenc", + install="pip install pylatexenc", +) +HAS_SEABORN = _LazyImportTester("seaborn", install="pip install seaborn") +HAS_SKLEARN = _LazyImportTester( + {"sklearn.linear_model": ("Ridge", "Lasso")}, + name="scikit-learn", + install="pip install scikit-learn", +) +HAS_SKQUANT = _LazyImportTester( + "skquant.opt", + name="scikit-quant", + install="pip install scikit-quant", +) +HAS_SQSNOBFIT = _LazyImportTester("SQSnobFit", install="pip install SQSnobFit") +HAS_SYMENGINE = _LazyImportTester("symengine", install="pip install symengine") +HAS_TESTTOOLS = _LazyImportTester("testtools", install="pip install testtools") +HAS_Z3 = _LazyImportTester("z3", install="pip install z3-solver") + +HAS_GRAPHVIZ = _LazySubprocessTester( + ("dot", "-V"), + name="graphviz", + install="'brew install graphviz' if on Mac, or by downloding it from their website", +) +HAS_PDFLATEX = _LazySubprocessTester( + ("pdflatex", "-version"), + msg="You will likely need to install a full LaTeX distribution for your system", +) +HAS_PDFTOCAIRO = _LazySubprocessTester( + ("pdftocairo", "-v"), + msg="This is part of the 'poppler' set of PDF utilities", +) diff --git a/qiskit/version.py b/qiskit/version.py index a61ade76d4ff..c277c12a1098 100644 --- a/qiskit/version.py +++ b/qiskit/version.py @@ -17,7 +17,6 @@ from collections.abc import Mapping import os import subprocess -import pkg_resources ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -103,6 +102,8 @@ def __init__(self): self._loaded = False def _load_versions(self): + import pkg_resources + try: from qiskit.providers import aer diff --git a/qiskit/visualization/__init__.py b/qiskit/visualization/__init__.py index cce0491f6819..41385caa1bbb 100644 --- a/qiskit/visualization/__init__.py +++ b/qiskit/visualization/__init__.py @@ -127,11 +127,13 @@ from qiskit.visualization.transition_visualization import visualize_transition from qiskit.visualization.array import array_to_latex -from .circuit_visualization import circuit_drawer, HAS_PIL, HAS_PDFLATEX, HAS_PDFTOCAIRO +# NOTE (Jake Lishman, 2022-01-12): for backwards compatibility. Deprecate these paths in Terra 0.21. +from qiskit.utils.optionals import HAS_MATPLOTLIB, HAS_PYLATEX, HAS_PIL, HAS_PDFTOCAIRO + +from .circuit_visualization import circuit_drawer from .dag_visualization import dag_drawer from .exceptions import VisualizationError from .gate_map import plot_gate_map, plot_circuit_layout, plot_error_map, plot_coupling_map -from .matplotlib import HAS_MATPLOTLIB, HAS_PYLATEX from .pass_manager_visualization import pass_manager_drawer from .pulse.interpolation import step_wise, linear, cubic_spline from .pulse.qcstyle import PulseStyle, SchedStyle diff --git a/qiskit/visualization/circuit_visualization.py b/qiskit/visualization/circuit_visualization.py index af091086cac2..4958819c1eba 100644 --- a/qiskit/visualization/circuit_visualization.py +++ b/qiskit/visualization/circuit_visualization.py @@ -30,15 +30,8 @@ import subprocess import tempfile -try: - from PIL import Image - - HAS_PIL = True -except ImportError: - HAS_PIL = False - from qiskit import user_config -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization import latex as _latex from qiskit.visualization import text as _text @@ -49,52 +42,6 @@ logger = logging.getLogger(__name__) -class _HasPdfLatexWrapper: - """Wrapper to lazily detect presence of the ``pdflatex`` command.""" - - def __init__(self): - self.has_pdflatex = None - - def __bool__(self): - if self.has_pdflatex is None: - try: - subprocess.run( - ["pdflatex", "-version"], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - self.has_pdflatex = True - except (OSError, subprocess.SubprocessError): - self.has_pdflatex = False - return self.has_pdflatex - - -class _HasPdfToCairoWrapper: - """Lazily detect the presence of the ``pdftocairo`` command.""" - - def __init__(self): - self.has_pdftocairo = None - - def __bool__(self): - if self.has_pdftocairo is None: - try: - subprocess.run( - ["pdftocairo", "-v"], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - self.has_pdftocairo = True - except (OSError, subprocess.SubprocessError): - self.has_pdftocairo = False - return self.has_pdftocairo - - -HAS_PDFLATEX = _HasPdfLatexWrapper() -HAS_PDFTOCAIRO = _HasPdfToCairoWrapper() - - def circuit_drawer( circuit, scale=None, @@ -231,7 +178,7 @@ def circuit_drawer( if config: default_output = config.get("circuit_drawer", "text") if default_output == "auto": - if _matplotlib.HAS_MATPLOTLIB: + if _optionals.HAS_MATPLOTLIB: default_output = "mpl" else: default_output = "text" @@ -393,6 +340,9 @@ def _text_circuit_drawer( # ----------------------------------------------------------------------------- +@_optionals.HAS_PDFLATEX.require_in_call("LaTeX circuit drawing") +@_optionals.HAS_PDFTOCAIRO.require_in_call("LaTeX circuit drawing") +@_optionals.HAS_PIL.require_in_call("LaTeX circuit drawing") def _latex_circuit_drawer( circuit, scale=0.7, @@ -437,6 +387,8 @@ def _latex_circuit_drawer( VisualizationError: if one of the conversion utilities failed for some internal or file-access reason. """ + from PIL import Image + tmpfilename = "circuit" with tempfile.TemporaryDirectory() as tmpdirname: tmppath = os.path.join(tmpdirname, tmpfilename + ".tex") @@ -453,24 +405,7 @@ def _latex_circuit_drawer( initial_state=initial_state, cregbundle=cregbundle, ) - if not HAS_PDFLATEX: - raise MissingOptionalLibraryError( - libname="pdflatex", - name="LaTeX circuit drawing", - msg="You will likely need to install a full LaTeX distribution for your system", - ) - if not HAS_PDFTOCAIRO: - raise MissingOptionalLibraryError( - libname="pdftocairo", - name="LaTeX circuit drawing", - msg="This is part of the 'poppler' set of PDF utilities", - ) - if not HAS_PIL: - raise MissingOptionalLibraryError( - libname="pillow", - name="LaTeX circuit drawing", - pip_install="pip install pillow", - ) + try: subprocess.run( [ diff --git a/qiskit/visualization/counts_visualization.py b/qiskit/visualization/counts_visualization.py index ad5cd4e8c03c..c8682a515b8f 100644 --- a/qiskit/visualization/counts_visualization.py +++ b/qiskit/visualization/counts_visualization.py @@ -18,8 +18,7 @@ import functools import numpy as np -from qiskit.exceptions import MissingOptionalLibraryError -from .matplotlib import HAS_MATPLOTLIB +from qiskit.utils import optionals as _optionals from .exceptions import VisualizationError from .utils import matplotlib_close_if_inline @@ -44,6 +43,7 @@ def hamming_distance(str1, str2): DIST_MEAS = {"hamming": hamming_distance} +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_histogram( data, figsize=(7, 5), @@ -108,12 +108,6 @@ def plot_histogram( job = execute(qc, backend) plot_histogram(job.result().get_counts(), color='midnightblue', title="New Histogram") """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_histogram", - pip_install="pip install matplotlib", - ) import matplotlib.pyplot as plt from matplotlib.ticker import MaxNLocator diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index 85ab227511bc..80a01e03a838 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -21,16 +21,10 @@ import tempfile from qiskit.dagcircuit.dagnode import DAGOpNode, DAGInNode, DAGOutNode -from qiskit.exceptions import MissingOptionalLibraryError, InvalidFileError +from qiskit.utils import optionals as _optionals +from qiskit.exceptions import InvalidFileError from .exceptions import VisualizationError -try: - from PIL import Image - - HAS_PIL = True -except ImportError: - HAS_PIL = False - FILENAME_EXTENSIONS = { "bmp", "canon", @@ -90,6 +84,7 @@ } +@_optionals.HAS_PYDOT.require_in_call def dag_drawer(dag, scale=0.7, filename=None, style="color"): """Plot the directed acyclic graph (dag) to represent operation dependencies in a quantum circuit. @@ -139,14 +134,8 @@ def dag_drawer(dag, scale=0.7, filename=None, style="color"): dag = circuit_to_dag(circ) dag_drawer(dag) """ - try: - import pydot - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="PyDot", - name="dag_drawer", - pip_install="pip install pydot", - ) from ex + import pydot + # NOTE: use type str checking to avoid potential cyclical import # the two tradeoffs ere that it will not handle subclasses and it is # slower (which doesn't matter for a visualization function) @@ -235,12 +224,8 @@ def edge_attr_func(edge): dot.write(filename, format=extension) return None elif ("ipykernel" in sys.modules) and ("spyder" not in sys.modules): - if not HAS_PIL: - raise MissingOptionalLibraryError( - libname="pillow", - name="dag_drawer", - pip_install="pip install pillow", - ) + _optionals.HAS_PIL.require_now("dag_drawer") + from PIL import Image with tempfile.TemporaryDirectory() as tmpdirname: tmp_path = os.path.join(tmpdirname, "dag.png") @@ -250,12 +235,9 @@ def edge_attr_func(edge): os.remove(tmp_path) return image else: - if not HAS_PIL: - raise MissingOptionalLibraryError( - libname="pillow", - name="dag_drawer", - pip_install="pip install pillow", - ) + _optionals.HAS_PIL.require_now("dag_drawer") + from PIL import Image + with tempfile.TemporaryDirectory() as tmpdirname: tmp_path = os.path.join(tmpdirname, "dag.png") dot.write_png(tmp_path) diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index 5794b1a2ceb5..5e6aea5c3ba8 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -15,12 +15,13 @@ import math from typing import List import numpy as np -from qiskit.exceptions import QiskitError, MissingOptionalLibraryError -from .matplotlib import HAS_MATPLOTLIB +from qiskit.exceptions import QiskitError +from qiskit.utils import optionals as _optionals from .exceptions import VisualizationError from .utils import matplotlib_close_if_inline +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_gate_map( backend, figsize=None, @@ -81,13 +82,6 @@ def plot_gate_map( backend = accountProvider.get_backend('ibmq_vigo') plot_gate_map(backend) """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_gate_map", - pip_install="pip install matplotlib", - ) - if backend.configuration().simulator: raise QiskitError("Requires a device backend, not simulator.") @@ -367,6 +361,7 @@ def plot_gate_map( ) +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_coupling_map( num_qubits: int, qubit_coordinates: List[List[int]], @@ -424,13 +419,6 @@ def plot_coupling_map( qubit_coordinates = [[0, 1], [1, 1], [1, 0], [1, 2], [2, 0], [2, 2], [2, 1], [3, 1]] plot_coupling_map(num_qubits, coupling_map, qubit_coordinates) """ - - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_coupling_map", - pip_install="pip install matplotlib", - ) import matplotlib.pyplot as plt import matplotlib.patches as mpatches @@ -673,6 +661,8 @@ def plot_circuit_layout(circuit, backend, view="virtual"): return fig +@_optionals.HAS_MATPLOTLIB.require_in_call +@_optionals.HAS_SEABORN.require_in_call def plot_error_map(backend, figsize=(12, 9), show_title=True): """Plots the error map of a given backend. @@ -708,20 +698,7 @@ def plot_error_map(backend, figsize=(12, 9), show_title=True): backend = provider.get_backend('ibmq_vigo') plot_error_map(backend) """ - try: - import seaborn as sns - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="seaborn", - name="plot_error_map", - pip_install="pip install seaborn", - ) from ex - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_error_map", - pip_install="pip install matplotlib", - ) + import seaborn as sns import matplotlib import matplotlib.pyplot as plt from matplotlib import gridspec, ticker diff --git a/qiskit/visualization/matplotlib.py b/qiskit/visualization/matplotlib.py index 816bc41e0668..7ef84dc298f7 100644 --- a/qiskit/visualization/matplotlib.py +++ b/qiskit/visualization/matplotlib.py @@ -19,14 +19,6 @@ import numpy as np - -try: - from pylatexenc.latex2text import LatexNodes2Text - - HAS_PYLATEX = True -except ImportError: - HAS_PYLATEX = False - from qiskit.circuit import ControlledGate from qiskit.circuit import Measure from qiskit.circuit.library.standard_gates import ( @@ -47,7 +39,7 @@ matplotlib_close_if_inline, ) from qiskit.circuit.tools.pi_check import pi_check -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals # Default gate width and height WID = 0.65 @@ -60,6 +52,8 @@ PORDER_TEXT = 6 +@_optionals.HAS_MATPLOTLIB.require_in_instance +@_optionals.HAS_PYLATEX.require_in_instance class MatplotlibDrawer: """Matplotlib drawer class called from circuit_drawer""" @@ -84,25 +78,11 @@ def __init__( cregs=None, calibrations=None, ): - - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="MatplotlibDrawer", - pip_install="pip install matplotlib", - ) from matplotlib import patches - - self._patches_mod = patches from matplotlib import pyplot as plt + self._patches_mod = patches self._plt_mod = plt - if not HAS_PYLATEX: - raise MissingOptionalLibraryError( - libname="pylatexenc", - name="MatplotlibDrawer", - pip_install="pip install pylatexenc", - ) # First load register and index info for the cregs and qregs, # then add any bits which don't have registers associated with them. @@ -579,6 +559,8 @@ def _get_coords(self, n_lines): def _get_text_width(self, text, fontsize, param=False, reg_to_remove=None): """Compute the width of a string in the default font""" + from pylatexenc.latex2text import LatexNodes2Text + if not text: return 0.0 @@ -1418,25 +1400,3 @@ def get_index(self): if self._gate_placed: return self._gate_placed[-1] + 1 return 0 - - -class HasMatplotlibWrapper: - """Wrapper to lazily import matplotlib.""" - - has_matplotlib = False - - # pylint: disable=unused-import - def __bool__(self): - if not self.has_matplotlib: - try: - from matplotlib import get_backend - from matplotlib import patches - from matplotlib import pyplot as plt - - self.has_matplotlib = True - except ImportError: - self.has_matplotlib = False - return self.has_matplotlib - - -HAS_MATPLOTLIB = HasMatplotlibWrapper() diff --git a/qiskit/visualization/pass_manager_visualization.py b/qiskit/visualization/pass_manager_visualization.py index 274a831e8722..edafc717c836 100644 --- a/qiskit/visualization/pass_manager_visualization.py +++ b/qiskit/visualization/pass_manager_visualization.py @@ -18,21 +18,16 @@ import inspect import tempfile -try: - from PIL import Image - - HAS_PIL = True -except ImportError: - HAS_PIL = False - +from qiskit.utils import optionals as _optionals from qiskit.visualization import utils from qiskit.visualization.exceptions import VisualizationError -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.transpiler.basepasses import AnalysisPass, TransformationPass DEFAULT_STYLE = {AnalysisPass: "red", TransformationPass: "blue"} +@_optionals.HAS_GRAPHVIZ.require_in_call +@_optionals.HAS_PYDOT.require_in_call def pass_manager_drawer(pass_manager, filename=None, style=None, raw=False): """ Draws the pass manager. @@ -78,37 +73,7 @@ def pass_manager_drawer(pass_manager, filename=None, style=None, raw=False): pass_manager_drawer(pm, "passmanager.jpg") """ - - try: - import subprocess - - with subprocess.Popen( - ["dot", "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) as _proc: - _proc.communicate() - if _proc.returncode != 0: - has_graphviz = False - else: - has_graphviz = True - except Exception: # pylint: disable=broad-except - # this is raised when the dot command cannot be found, which means GraphViz - # isn't installed - has_graphviz = False - - HAS_GRAPHVIZ = has_graphviz # pylint: disable=invalid-name - - if not HAS_GRAPHVIZ: - raise MissingOptionalLibraryError( - libname="graphviz", - name="pass_manager_drawer", - pip_install="'brew install graphviz' on Mac or by downloading it from the website.", - ) - try: - import pydot - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="pydot", name="pass_manager_drawer", pip_install="pip install pydot" - ) from ex + import pydot passes = pass_manager.passes() @@ -193,12 +158,16 @@ def pass_manager_drawer(pass_manager, filename=None, style=None, raw=False): else: raise VisualizationError("if format=raw, then a filename is required.") - if not HAS_PIL and filename: + if not _optionals.HAS_PIL and filename: # pylint says this isn't a method - it is graph.write_png(filename) # pylint: disable=no-member return None + _optionals.HAS_PIL.require_now("pass manager drawer") + with tempfile.TemporaryDirectory() as tmpdirname: + from PIL import Image + tmppath = os.path.join(tmpdirname, "pass_manager.png") # pylint says this isn't a method - it is diff --git a/qiskit/visualization/pulse/interpolation.py b/qiskit/visualization/pulse/interpolation.py index 2706f715fa53..0bb3617b65e9 100644 --- a/qiskit/visualization/pulse/interpolation.py +++ b/qiskit/visualization/pulse/interpolation.py @@ -19,7 +19,6 @@ from typing import Tuple import numpy as np -from scipy import interpolate def interp1d( @@ -38,6 +37,8 @@ def interp1d( Returns: Interpolated time vector and real and imaginary part of waveform. """ + from scipy import interpolate + re_y = np.real(samples) im_y = np.imag(samples) diff --git a/qiskit/visualization/pulse/matplotlib.py b/qiskit/visualization/pulse/matplotlib.py index 5f4ab6a0694d..06cec2a75473 100644 --- a/qiskit/visualization/pulse/matplotlib.py +++ b/qiskit/visualization/pulse/matplotlib.py @@ -19,8 +19,7 @@ import numpy as np -from qiskit.visualization.matplotlib import HAS_MATPLOTLIB -from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.utils import optionals as _optionals from qiskit.visualization.pulse.qcstyle import PulseStyle, SchedStyle from qiskit.visualization.pulse.interpolation import step_wise from qiskit.pulse.channels import ( @@ -287,6 +286,7 @@ def __init__(self, style: PulseStyle): """ self.style = style or PulseStyle() + @_optionals.HAS_MATPLOTLIB.require_in_call("waveform drawer") def draw( self, pulse: Waveform, @@ -310,17 +310,10 @@ def draw( Raises: MissingOptionalLibraryError: If matplotlib is not installed """ - # If these self.style.dpi or self.style.figsize are None, they will - # revert back to their default rcParam keys. - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="WaveformDrawer", - pip_install="pip install matplotlib", - ) - from matplotlib import pyplot as plt + # If these self.style.dpi or self.style.figsize are None, they will + # revert back to their default rcParam keys. figure = plt.figure(dpi=self.style.dpi, figsize=self.style.figsize) interp_method = interp_method or step_wise @@ -376,6 +369,7 @@ def draw( return figure +@_optionals.HAS_MATPLOTLIB.require_in_instance class ScheduleDrawer: """A class to create figure for schedule and channel.""" @@ -387,18 +381,10 @@ def __init__(self, style: SchedStyle): Raises: MissingOptionalLibraryError: If matplotlib is not installed """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="ScheduleDrawer", - pip_install="pip install matplotlib", - ) - from matplotlib import pyplot as plt - - self.plt_mod = plt from matplotlib import gridspec + self.plt_mod = plt self.gridspec_mod = gridspec self.style = style or SchedStyle() diff --git a/qiskit/visualization/pulse_visualization.py b/qiskit/visualization/pulse_visualization.py index 62a1130c95a9..fa5485a625ce 100644 --- a/qiskit/visualization/pulse_visualization.py +++ b/qiskit/visualization/pulse_visualization.py @@ -17,16 +17,16 @@ from typing import Union, Callable, List, Dict, Tuple -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.pulse import Schedule, Instruction, Waveform from qiskit.pulse.channels import Channel +from qiskit.utils import optionals as _optionals from qiskit.visualization.pulse.qcstyle import PulseStyle, SchedStyle from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization.pulse import matplotlib as _matplotlib -from qiskit.visualization.matplotlib import HAS_MATPLOTLIB from qiskit.visualization.utils import matplotlib_close_if_inline +@_optionals.HAS_MATPLOTLIB.require_in_call def pulse_drawer( data: Union[Waveform, Union[Schedule, Instruction]], dt: int = 1, @@ -154,12 +154,6 @@ def pulse_drawer( DeprecationWarning, ) - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="pulse_drawer", - pip_install="pip install matplotlib", - ) if isinstance(data, Waveform): drawer = _matplotlib.WaveformDrawer(style=style) image = drawer.draw(data, dt=dt, interp_method=interp_method, scale=scale) diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index dda2e3d67c12..879ca1f598e6 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -21,14 +21,12 @@ from functools import reduce import colorsys import numpy as np -from scipy import linalg from qiskit import user_config -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.quantum_info.states.statevector import Statevector from qiskit.quantum_info.states.densitymatrix import DensityMatrix from qiskit.visualization.array import array_to_latex from qiskit.utils.deprecation import deprecate_arguments -from qiskit.visualization.matplotlib import HAS_MATPLOTLIB +from qiskit.utils import optionals as _optionals from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization.utils import ( _bloch_multivector_data, @@ -39,6 +37,7 @@ @deprecate_arguments({"rho": "state"}) +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_state_hinton( state, title="", figsize=None, ax_real=None, ax_imag=None, *, rho=None, filename=None ): @@ -86,12 +85,6 @@ def plot_state_hinton( state = DensityMatrix.from_instruction(qc) plot_state_hinton(state, title="New Hinton Plot") """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_state_hinton", - pip_install="pip install matplotlib", - ) from matplotlib import pyplot as plt # Figure data @@ -181,6 +174,7 @@ def plot_state_hinton( return fig.savefig(filename) +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartesian"): """Plot the Bloch sphere. @@ -212,12 +206,6 @@ def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartes plot_bloch_vector([0,1,0], title="New Bloch Sphere") """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_bloch_vector", - pip_install="pip install matplotlib", - ) from qiskit.visualization.bloch import Bloch if figsize is None: @@ -239,6 +227,7 @@ def plot_bloch_vector(bloch, title="", ax=None, figsize=None, coord_type="cartes @deprecate_arguments({"rho": "state"}) +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_bloch_multivector( state, title="", figsize=None, *, rho=None, reverse_bits=False, filename=None ): @@ -275,12 +264,6 @@ def plot_bloch_multivector( state = Statevector.from_instruction(qc) plot_bloch_multivector(state) """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_bloch_multivector", - pip_install="pip install matplotlib", - ) from matplotlib import pyplot as plt # Data @@ -303,6 +286,7 @@ def plot_bloch_multivector( @deprecate_arguments({"rho": "state"}) +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_state_city( state, title="", @@ -366,12 +350,6 @@ def plot_state_city( plot_state_city(state, color=['midnightblue', 'midnightblue'], title="New State City") """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_state_city", - pip_install="pip install matplotlib", - ) from matplotlib import pyplot as plt from mpl_toolkits.mplot3d.art3d import Poly3DCollection @@ -538,6 +516,7 @@ def plot_state_city( @deprecate_arguments({"rho": "state"}) +@_optionals.HAS_MATPLOTLIB.require_in_call def plot_state_paulivec( state, title="", figsize=None, color=None, ax=None, *, rho=None, filename=None ): @@ -580,12 +559,6 @@ def plot_state_paulivec( plot_state_paulivec(state, color='midnightblue', title="New PauliVec plot") """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_state_paulivec", - pip_install="pip install matplotlib", - ) from matplotlib import pyplot as plt labels, values = _paulivec_data(state) @@ -684,6 +657,8 @@ def phase_to_rgb(complex_number): @deprecate_arguments({"rho": "state"}) +@_optionals.HAS_MATPLOTLIB.require_in_call +@_optionals.HAS_SEABORN.require_in_call def plot_state_qsphere( state, figsize=None, @@ -738,26 +713,13 @@ def plot_state_qsphere( state = Statevector.from_instruction(qc) plot_state_qsphere(state) """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_state_qsphere", - pip_install="pip install matplotlib", - ) - from matplotlib import gridspec from matplotlib import pyplot as plt from matplotlib.patches import Circle + import seaborn as sns + from scipy import linalg from qiskit.visualization.bloch import Arrow3D - try: - import seaborn as sns - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="seaborn", - name="plot_state_qsphere", - pip_install="pip install seaborn", - ) from ex rho = DensityMatrix(state) num = rho.num_qubits if num is None: @@ -961,6 +923,7 @@ def plot_state_qsphere( return fig.savefig(filename) +@_optionals.HAS_MATPLOTLIB.require_in_call def generate_facecolors(x, y, z, dx, dy, dz, color): """Generates shaded facecolors for shaded bars. @@ -980,12 +943,6 @@ def generate_facecolors(x, y, z, dx, dy, dz, color): Raises: MissingOptionalLibraryError: If matplotlib is not installed """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_state_city", - pip_install="pip install matplotlib", - ) import matplotlib.colors as mcolors cuboid = np.array( @@ -1106,13 +1063,6 @@ def _shade_colors(color, normals, lightsource=None): Shade *color* using normal vectors given by *normals*. *color* can also be an array of the same length as *normals*. """ - if not HAS_MATPLOTLIB: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_state_city", - pip_install="pip install matplotlib", - ) - from matplotlib.colors import Normalize, LightSource import matplotlib.colors as mcolors @@ -1410,20 +1360,11 @@ def state_drawer(state, output=None, **drawer_args): "paulivec": plot_state_paulivec, } if output == "latex": - try: - from IPython.display import Latex - except ImportError as err: - raise MissingOptionalLibraryError( - libname="IPython", - name="state_drawer", - pip_install=( - "\"pip install ipython\", or set output='latex_source' " - "instead for an ASCII string." - ), - ) from err - else: - draw_func = drawers["latex_source"] - return Latex(f"$${draw_func(state, **drawer_args)}$$") + _optionals.HAS_IPYTHON.require_now("state_drawer") + from IPython.display import Latex + + draw_func = drawers["latex_source"] + return Latex(f"$${draw_func(state, **drawer_args)}$$") if output == "repr": return state.__repr__() diff --git a/qiskit/visualization/utils.py b/qiskit/visualization/utils.py index 1d4a4858fde4..a7e5b9550493 100644 --- a/qiskit/visualization/utils.py +++ b/qiskit/visualization/utils.py @@ -30,25 +30,11 @@ from qiskit.circuit.library import PauliEvolutionGate from qiskit.circuit.tools import pi_check from qiskit.converters import circuit_to_dag -from qiskit.exceptions import MissingOptionalLibraryError from qiskit.quantum_info.operators.symplectic import PauliList, SparsePauliOp from qiskit.quantum_info.states import DensityMatrix +from qiskit.utils import optionals as _optionals from qiskit.visualization.exceptions import VisualizationError -try: - import PIL - - HAS_PIL = True -except ImportError: - HAS_PIL = False - -try: - from pylatexenc.latexencode import utf8tolatex - - HAS_PYLATEX = True -except ImportError: - HAS_PYLATEX = False - def get_gate_ctrl_text(op, drawer, style=None, calibrations=None): """Load the gate_text and ctrl_text strings based on names and labels""" @@ -294,14 +280,10 @@ def fix_special_characters(label): return label +@_optionals.HAS_PYLATEX.require_in_call("the latex and latex_source circuit drawers") def generate_latex_label(label): """Convert a label to a valid latex string.""" - if not HAS_PYLATEX: - raise MissingOptionalLibraryError( - libname="pylatexenc", - name="the latex and latex_source circuit drawers", - pip_install="pip install pylatexenc", - ) + from pylatexenc.latexencode import utf8tolatex regex = re.compile(r"(? None: self.backend = ConfigurableFakeBackend("Tashkent", n_qubits=4) @unittest.skip("Skipped until qiskit-aer#741 is fixed and released") - @unittest.skipUnless(HAS_AER, "qiskit-aer is required to run this test") + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def test_transpile_schedule_and_assemble(self): """Test transpile, schedule and assemble on generated backend.""" qc = get_test_circuit() @@ -70,7 +69,7 @@ def test_transpile_schedule_and_assemble(self): class FakeBackendsTest(QiskitTestCase): """fake backends test.""" - @unittest.skipUnless(HAS_AER, "qiskit-aer is required to run this test") + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def test_fake_backends_get_kwargs(self): """Fake backends honor kwargs passed.""" backend = FakeAthens() diff --git a/test/python/opflow/test_gradients.py b/test/python/opflow/test_gradients.py index ff7f3ebb70cf..890c10281d69 100644 --- a/test/python/opflow/test_gradients.py +++ b/test/python/opflow/test_gradients.py @@ -19,13 +19,6 @@ import numpy as np from ddt import ddt, data, idata, unpack -try: - import jax.numpy as jnp - - _HAS_JAX = True -except ImportError: - _HAS_JAX = False - from qiskit import QuantumCircuit, QuantumRegister, BasicAer from qiskit.test import slow_test from qiskit.utils import QuantumInstance @@ -51,6 +44,10 @@ from qiskit.circuit import Parameter from qiskit.circuit import ParameterVector from qiskit.circuit.library import RealAmplitudes, EfficientSU2 +from qiskit.utils import optionals + +if optionals.HAS_JAX: + import jax.numpy as jnp @ddt @@ -479,7 +476,7 @@ def test_state_hessian(self, method): state_hess.assign_parameters(value_dict).eval(), correct_values[i], decimal=1 ) - @unittest.skipIf(not _HAS_JAX, "Skipping test due to missing jax module.") + @unittest.skipIf(not optionals.HAS_JAX, "Skipping test due to missing jax module.") @data("lin_comb", "param_shift", "fin_diff") def test_state_hessian_custom_combo_fn(self, method): """Test the state Hessian with on an operator which includes @@ -688,7 +685,7 @@ def test_natural_gradient4(self, grad_method, qfi_method, regularization): except MissingOptionalLibraryError as ex: self.skipTest(str(ex)) - @unittest.skipIf(not _HAS_JAX, "Skipping test due to missing jax module.") + @unittest.skipIf(not optionals.HAS_JAX, "Skipping test due to missing jax module.") @idata(product(["lin_comb", "param_shift", "fin_diff"], [True, False])) @unpack def test_jax_chain_rule(self, method: str, autograd: bool): diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index e9d2c90bddf6..04560320e423 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -25,7 +25,7 @@ from qiskit.execute_function import execute from qiskit.test.base import QiskitTestCase from qiskit.test.mock import FakeProvider, FakeLegacyProvider -from qiskit.test.mock.fake_backend import HAS_AER +from qiskit.utils import optionals FAKE_PROVIDER = FakeProvider() @@ -51,7 +51,7 @@ def setUpClass(cls): optimization_level=[0, 1, 2, 3], ) def test_circuit_on_fake_backend(self, backend, optimization_level): - if not HAS_AER and backend.configuration().num_qubits > 20: + if not optionals.HAS_AER and backend.configuration().num_qubits > 20: self.skipTest( "Unable to run fake_backend %s without qiskit-aer" % backend.configuration().backend_name @@ -73,7 +73,7 @@ def test_circuit_on_fake_backend(self, backend, optimization_level): optimization_level=[0, 1, 2, 3], ) def test_circuit_on_fake_legacy_backend(self, backend, optimization_level): - if not HAS_AER and backend.configuration().num_qubits > 20: + if not optionals.HAS_AER and backend.configuration().num_qubits > 20: self.skipTest( "Unable to run fake_backend %s without qiskit-aer" % backend.configuration().backend_name diff --git a/test/python/tools/jupyter/test_notebooks.py b/test/python/tools/jupyter/test_notebooks.py index d0239081974a..9a4e5a9f2427 100644 --- a/test/python/tools/jupyter/test_notebooks.py +++ b/test/python/tools/jupyter/test_notebooks.py @@ -18,7 +18,7 @@ import nbformat from nbconvert.preprocessors import ExecutePreprocessor -from qiskit.tools.visualization import HAS_MATPLOTLIB +from qiskit.utils import optionals from qiskit.test import Path, QiskitTestCase, slow_test @@ -28,6 +28,7 @@ JUPYTER_KERNEL = "python3" +@unittest.skipUnless(optionals.HAS_IBMQ, "requires IBMQ provider") class TestJupyter(QiskitTestCase): """Notebooks test case.""" @@ -49,6 +50,7 @@ def _execute_notebook(self, filename): top_str = """ import qiskit + import qiskit.providers.ibmq import sys from unittest.mock import create_autospec, MagicMock from qiskit.test.mock import FakeProviderFactory @@ -83,7 +85,7 @@ def test_jupyter_jobs_pbars(self): """Test Jupyter progress bars and job status functionality""" self._execute_notebook(os.path.join(self.notebook_dir, "test_pbar_status.ipynb")) - @unittest.skipIf(not HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") @slow_test def test_backend_tools(self): """Test Jupyter backend tools.""" diff --git a/test/python/transpiler/test_bip_mapping.py b/test/python/transpiler/test_bip_mapping.py index 917d92bcb414..cf0575862268 100644 --- a/test/python/transpiler/test_bip_mapping.py +++ b/test/python/transpiler/test_bip_mapping.py @@ -24,25 +24,11 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BIPMapping from qiskit.transpiler.passes import CheckMap, Collect2qBlocks, ConsolidateBlocks, UnitarySynthesis +from qiskit.utils import optionals -try: - import cplex # pylint: disable=unused-import - - HAS_CPLEX = True -except ImportError: - HAS_CPLEX = False - -try: - import docplex # pylint: disable=unused-import - - HAS_DOCPLEX = True -except ImportError: - HAS_DOCPLEX = False - - -@unittest.skipUnless(HAS_CPLEX, "cplex is required to run the BIPMapping tests") -@unittest.skipUnless(HAS_DOCPLEX, "docplex is required to run the BIPMapping tests") +@unittest.skipUnless(optionals.HAS_CPLEX, "cplex is required to run the BIPMapping tests") +@unittest.skipUnless(optionals.HAS_DOCPLEX, "docplex is required to run the BIPMapping tests") class TestBIPMapping(QiskitTestCase): """Tests the BIPMapping pass.""" diff --git a/test/python/transpiler/test_crosstalk_adaptive_scheduler.py b/test/python/transpiler/test_crosstalk_adaptive_scheduler.py index e07d29059901..1847cf35f059 100644 --- a/test/python/transpiler/test_crosstalk_adaptive_scheduler.py +++ b/test/python/transpiler/test_crosstalk_adaptive_scheduler.py @@ -19,12 +19,12 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.transpiler import Layout from qiskit.transpiler.passes.optimization import CrosstalkAdaptiveSchedule -from qiskit.transpiler.passes.optimization.crosstalk_adaptive_schedule import HAS_Z3 from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase from qiskit.compiler import transpile from qiskit.providers.models import BackendProperties from qiskit.providers.models.backendproperties import Nduv, Gate +from qiskit.utils import optionals def make_noisy_qubit(t_1=50.0, t_2=50.0): @@ -137,7 +137,7 @@ def create_fake_machine(): return bprop -@unittest.skipIf(not HAS_Z3, "z3-solver not installed.") +@unittest.skipIf(not optionals.HAS_Z3, "z3-solver not installed.") class TestCrosstalk(QiskitTestCase): """ Tests for crosstalk adaptivity diff --git a/test/python/transpiler/test_hoare_opt.py b/test/python/transpiler/test_hoare_opt.py index 56426a4ecee5..44213c2c1487 100644 --- a/test/python/transpiler/test_hoare_opt.py +++ b/test/python/transpiler/test_hoare_opt.py @@ -14,7 +14,7 @@ import unittest from numpy import pi -from qiskit.transpiler.passes.optimization.hoare_opt import HAS_Z3 +from qiskit.utils import optionals from qiskit.transpiler.passes import HoareOptimizer from qiskit.converters import circuit_to_dag from qiskit import QuantumCircuit @@ -24,7 +24,7 @@ from qiskit.quantum_info import Statevector -@unittest.skipUnless(HAS_Z3, "z3-solver needs to be installed to run these tests") +@unittest.skipUnless(optionals.HAS_Z3, "z3-solver needs to be installed to run these tests") class TestHoareOptimizer(QiskitTestCase): """Test the HoareOptimizer pass""" diff --git a/test/python/utils/mitigation/test_meas.py b/test/python/utils/mitigation/test_meas.py index 2b2188de0e36..b9375edb6ea8 100644 --- a/test/python/utils/mitigation/test_meas.py +++ b/test/python/utils/mitigation/test_meas.py @@ -42,15 +42,14 @@ from qiskit.utils.mitigation._filters import MeasurementFilter from qiskit.utils.mitigation.circuits import count_keys -try: +from qiskit.utils import optionals + +if optionals.HAS_AER: + # pylint: disable=import-error,no-name-in-module from qiskit.providers.aer import Aer from qiskit.providers.aer.noise import NoiseModel from qiskit.providers.aer.noise.errors.standard_errors import pauli_error - HAS_AER = True -except ImportError: - HAS_AER = False - # fixed seed for tests - for both simulator and transpiler SEED = 42 @@ -206,7 +205,7 @@ def tensored_calib_circ_execution(shots: int, seed: int): return cal_results, mit_pattern, ghz_results, meas_layout -@unittest.skipUnless(HAS_AER, "Qiskit aer is required to run these tests") +@unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") class TestMeasCal(QiskitTestCase): """The test class.""" diff --git a/test/python/utils/test_classtools.py b/test/python/utils/test_classtools.py new file mode 100644 index 000000000000..272c3e0a6146 --- /dev/null +++ b/test/python/utils/test_classtools.py @@ -0,0 +1,533 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the methods in ``utils.classtools``.""" + +# If `utils.wrap_method` is horrendously broken, then the test suite probably won't even have been +# able to collect, because it's used in `QiskitTestCase`. +# +# Throughout this file, we use ``this`` as an argument name to refer to a general reference, which +# may be an instance or the type of the instance. This is just for clarity when we are using the +# same function to wrap both instance and class methods. +# +# We use a lot of dummy classes in test cases, for which there is absolutely no point defining +# docstrings. +# pylint: disable=missing-class-docstring,missing-function-docstring + +import unittest +import sys + +from qiskit.utils import wrap_method +from qiskit.test import QiskitTestCase + + +def call_first_argument_with(*args, **kwargs): + """Create a function that calls its first and only argument with the signature given to this + function.""" + + def out(mock): + return mock(*args, **kwargs) + + return out + + +def call_second_argument_with(*args, **kwargs): + """Create a function that calls its second argument with the first, and the signature given to + this function.""" + + def out(this, mock): + mock(this, *args, **kwargs) + + return out + + +class TestWrapMethod(QiskitTestCase): + """Tests of ``utils.classtools.wrap_method``. These can be rather tricky, because there's a lot + of messing around with descriptors and some special cases necessary to support builtin Python + functions, so we sometimes have rather funny access patterns.""" + + def test_called_with(self): + """Test the basic call patterns are correct. We use regular Python functions rather than + mocks to make the instances and callbacks in this simplest case, because the low-level + descriptor use means that there might be side-effects to binding mocks directly.""" + + class Dummy: + def instance(self, mock): + mock(self, "method") + + @classmethod + def class_(cls, mock): + mock(cls, "method") + + @staticmethod + def static(mock): + mock("method") + + wrap_method( + Dummy, + "instance", + before=call_second_argument_with("before"), + after=call_second_argument_with("after"), + ) + wrap_method( + Dummy, + "class_", + before=call_second_argument_with("before"), + after=call_second_argument_with("after"), + ) + wrap_method( + Dummy, + "static", + before=call_first_argument_with("before"), + after=call_first_argument_with("after"), + ) + + with self.subTest("from instance"): + source = Dummy() + with self.subTest("instance"): + mock = unittest.mock.Mock() + + # Test that the before/after are not called when the method is accessed. + caller = source.instance + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [ + unittest.mock.call(source, "before"), + unittest.mock.call(source, "method"), + unittest.mock.call(source, "after"), + ], + any_order=False, + ) + with self.subTest("class"): + mock = unittest.mock.Mock() + caller = source.class_ + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [ + unittest.mock.call(Dummy, "before"), + unittest.mock.call(Dummy, "method"), + unittest.mock.call(Dummy, "after"), + ], + any_order=False, + ) + with self.subTest("static"): + mock = unittest.mock.Mock() + caller = source.static + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [ + unittest.mock.call("before"), + unittest.mock.call("method"), + unittest.mock.call("after"), + ], + any_order=False, + ) + + with self.subTest("from type"): + with self.subTest("instance"): + mock = unittest.mock.Mock() + + # Test that the before/after are not called when the method is accessed. + caller = Dummy.instance + mock.assert_not_called() + caller("this", mock) + mock.assert_has_calls( + [ + unittest.mock.call("this", "before"), + unittest.mock.call("this", "method"), + unittest.mock.call("this", "after"), + ], + any_order=False, + ) + with self.subTest("class"): + mock = unittest.mock.Mock() + caller = Dummy.class_ + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [ + unittest.mock.call(Dummy, "before"), + unittest.mock.call(Dummy, "method"), + unittest.mock.call(Dummy, "after"), + ], + any_order=False, + ) + with self.subTest("static"): + mock = unittest.mock.Mock() + caller = Dummy.static + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [ + unittest.mock.call("before"), + unittest.mock.call("method"), + unittest.mock.call("after"), + ], + any_order=False, + ) + + def test_can_wrap_with_lambda(self): + """Test that lambda functions can be used as the callbacks.""" + + class Dummy: + def instance(self, mock): + mock(self, "method") + + wrap_method(Dummy, "instance", after=lambda self, mock: mock(self, "after")) + with self.subTest("from instance"): + mock = unittest.mock.Mock() + source = Dummy() + caller = source.instance + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [unittest.mock.call(source, "method"), unittest.mock.call(source, "after")], + any_order=False, + ) + + with self.subTest("from type"): + mock = unittest.mock.Mock() + caller = Dummy.instance + mock.assert_not_called() + caller("this", mock) + mock.assert_has_calls( + [unittest.mock.call("this", "method"), unittest.mock.call("this", "after")], + any_order=False, + ) + + def test_can_wrap_with_callable_class(self): + """Test that a class with a ``__call__`` but no descriptor protocol can be used as the + callbacks.""" + + class Dummy: + def instance(self, mock): + mock(self, "method") + + class Callback: + # Note that this class does not implement the descriptor protocol, unlike normal Python + # functions. ``__call__`` itself implements ``__get__``, but that just bounds the + # ``Callback`` instance to ``self``. + def __call__(self, this, mock): + mock(this, "after") + + wrap_method(Dummy, "instance", after=Callback()) + with self.subTest("from instance"): + mock = unittest.mock.Mock() + source = Dummy() + caller = source.instance + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [unittest.mock.call(source, "method"), unittest.mock.call(source, "after")], + any_order=False, + ) + + with self.subTest("from type"): + mock = unittest.mock.Mock() + caller = Dummy.instance + mock.assert_not_called() + caller("this", mock) + mock.assert_has_calls( + [unittest.mock.call("this", "method"), unittest.mock.call("this", "after")], + any_order=False, + ) + + def test_can_wrap_with_builtin(self): + """Test that builtin functions can be used a callback. Many CPython builtins don't + implement the desriptor protocol that all functions defined with ``def`` or ``lambda`` do, + which means we need to take special care that they work. This is most relevant for + C-extension functions created via pybind11, Cython or similar, rather than actual Python + builtins.""" + + class Dummy: + def instance(self, mock): + mock(self, "method") + + # We don't want to add additional compilation requirements for one simple test in the suite, + # so instead we need a CPython builtin function (of type ``types.BuiltinFunctionType``) that + # has side effects when being called with an arbitrary object as the first positional + # argument. ``breakpoint`` is a good choice, because it's designed to support dependency + # injection with arbitrary arguments; it calls out to the overridable ``sys.breakpointhook`` + # with all its arguments. We can't use ``eval`` to test the bound-method calls because the + # first argument would be the instance, not the string with a side-effect-y programme. + with unittest.mock.patch.object(sys, "breakpointhook") as mock: + wrap_method(Dummy, "instance", before=breakpoint) + + with self.subTest("from instance"): + mock.reset_mock() + source = Dummy() + source.instance(mock) + mock.assert_has_calls( + [ + unittest.mock.call(source, mock), + unittest.mock.call(source, "method"), + ] + ) + + with self.subTest("from type"): + mock.reset_mock() + Dummy.instance("this", mock) + mock.assert_has_calls( + [ + unittest.mock.call("this", mock), + unittest.mock.call("this", "method"), + ] + ) + + def test_can_wrap_with_mock(self): + """This is kind of a meta test, to check that we can use a ``unittest.mock.Mock`` instance + as the callback.""" + + class Dummy: + def instance(self, x): + pass + + mock = unittest.mock.Mock() + wrap_method(Dummy, "instance", before=mock) + + with self.subTest("from instance"): + mock.reset_mock() + source = Dummy() + source.instance("hello, world") + mock.assert_called_once_with(source, "hello, world") + with self.subTest("from type"): + mock.reset_mock() + Dummy.instance("this", "hello, world") + mock.assert_called_once_with("this", "hello, world") + + def test_wrapping_inherited_method(self): + """Test that ``wrap_method`` will correctly find a method defined only on a parent class.""" + + class Parent: + def instance(self, mock): + mock(self, "method") + + class Child(Parent): + pass + + wrap_method(Child, "instance", before=call_second_argument_with("before")) + + with self.subTest("from instance"): + mock = unittest.mock.Mock() + source = Child() + caller = source.instance + mock.assert_not_called() + caller(mock) + mock.assert_has_calls( + [unittest.mock.call(source, "before"), unittest.mock.call(source, "method")], + any_order=False, + ) + + with self.subTest("from type"): + mock = unittest.mock.Mock() + caller = Child.instance + mock.assert_not_called() + caller("this", mock) + mock.assert_has_calls( + [unittest.mock.call("this", "before"), unittest.mock.call("this", "method")], + any_order=False, + ) + + def test_wrapping___init__(self): + """Test that wrapping the magic __init__ method works.""" + + class Dummy: + def __init__(self, mock): + mock("__init__") + self.mock = mock + + def add_extra_property(self, _): + mock("extra") + self.extra = "hello, world" + + wrap_method(Dummy, "__init__", after=add_extra_property) + + mock = unittest.mock.Mock() + dummy = Dummy(mock) + self.assertIs(dummy.mock, mock) + self.assertEqual(dummy.extra, "hello, world") # pylint: disable=no-member + mock.assert_has_calls( + [unittest.mock.call("__init__"), unittest.mock.call("extra")], + any_order=False, + ) + + def test_wrapping___add__(self): + """Test that wrapping an arithmetic operator works. There is nothing particularly special + about ``__add__`` that (say) ``__init__`` doesn't also do, but this is just a further check + that the magic methods can work. Note that ``__add__`` must be defined on the type; all + magic methods ignore re-definitions in instance dictionaries.""" + + mock = unittest.mock.Mock() + + class Dummy: + def __init__(self, n): + self.n = n + + def __add__(self, other): + return type(self)(self.n + other.n) + + wrap_method(Dummy, "__add__", before=mock) + + left = Dummy(1) + right = Dummy(2) + out = left + right + self.assertIsInstance(out, Dummy) + self.assertEqual(out.n, 3) + mock.assert_has_calls([unittest.mock.call(left, right)]) + + def test_wrapping___new__(self): + """Test that wrapping the magic __new__ method works. This method is implicitly made into a + static method with no decorator, but still gets called with the class in the first + position. Note that ``type`` implements ``__new__``, so the getter needs to ensure that it + doesn't accidentally""" + + class Dummy: + def __new__(cls, mock): + mock(cls, "__new__") + return super().__new__(cls) + + wrap_method(Dummy, "__new__", before=call_second_argument_with("extra")) + + mock = unittest.mock.Mock() + dummy = Dummy(mock) + mock.assert_has_calls( + [unittest.mock.call(Dummy, "extra"), unittest.mock.call(Dummy, "__new__")], + any_order=False, + ) + + def test_wrapping_object___new__(self): + """Test that wrapping the magic __new__ method works when it is inherited. This is a very + special case, because by inheritance ``A.__new__`` is ``type.__new__``, but that's not we + want to wrap; we need to have used ``type.__getattribute__`` to make sure that we're getting + the default implementation ``object.__new__``, and not the ``__new__`` method that literally + constructs new types. + """ + + class Dummy: + pass + + mock = unittest.mock.Mock() + wrap_method(Dummy, "__new__", before=mock) + dummy = Dummy() + mock.assert_called_once_with(Dummy) + + def test_wrapping_object___eq__(self): + """Test that wrapping equality works. ``type`` also implements ``__eq__`` in a way that + returns ``NotImplemented`` if one of the operands is not a ``type``, so this tests that we + are successfully finding ``object.__eq__`` in the resolution of the wrapped method.""" + + class Dummy: + def __init__(self, n): + self.n = n + + def __eq__(self, other): + return self.n == other.n + + mock = unittest.mock.Mock() + wrap_method(Dummy, "__eq__", before=mock) + + left = Dummy(1) + right = Dummy(2) + self.assertNotEqual(left, right) + mock.assert_has_calls([unittest.mock.call(left, right)]) + + def test_wrapping_object___init_subclass__(self): + """Test that wrapping the magic ``__init_subclass__`` method works. This method is + implicitly made into a class method without needing a decorator.""" + + class Dummy: + pass + + mock = unittest.mock.Mock() + wrap_method(Dummy, "__init_subclass__", before=mock) + + class Child(Dummy): + pass + + mock.assert_called_once_with(Child) + + def test_wrapping_already_wrapped_method(self): + """Test that a chain of wrapped methods evaluate correctly, in the right order. Methods + that are explicitly overridden in child class definitions do not create chains of wrapped + methods in the same way, even if they call ``super().method`` because the actual object in + the child definition would be a regular function. This tests the case that method we're + wrapping is the exact output of a previous wrapping.""" + + class Grandparent: + def method(self, mock): + mock(self, "grandparent") + + wrap_method( + Grandparent, + "method", + before=call_second_argument_with("before 1"), + after=call_second_argument_with("after 1"), + ) + + class Parent(Grandparent): + pass + + wrap_method( + Parent, + "method", + before=call_second_argument_with("before 2"), + after=call_second_argument_with("after 2"), + ) + + class Child(Parent): + pass + + wrap_method( + Child, + "method", + before=call_second_argument_with("before 3"), + after=call_second_argument_with("after 3"), + ) + + mock = unittest.mock.Mock() + child = Child() + child.method(mock) + mock.assert_has_calls( + [ + unittest.mock.call(child, "before 3"), + unittest.mock.call(child, "before 2"), + unittest.mock.call(child, "before 1"), + unittest.mock.call(child, "grandparent"), + unittest.mock.call(child, "after 1"), + unittest.mock.call(child, "after 2"), + unittest.mock.call(child, "after 3"), + ], + any_order=False, + ) + + def test_docstring_inherited(self): + """Test that the docstring of a method is correctly passed through, to avoid clobbering + documentation.""" + + class Dummy: + def method(self): + """This is documentation.""" + + wrap_method(Dummy, "method", before=lambda self: None) + self.assertEqual(Dummy.method.__doc__, "This is documentation.") + + def test_raises_on_invalid_name(self): + """Test that a suitable error is raised if the method doesn't exist.""" + + class Dummy: + pass + + with self.assertRaisesRegex(ValueError, "Method 'bad' is not defined for class 'Dummy'"): + wrap_method(Dummy, "bad", before=lambda self: None) diff --git a/test/python/utils/test_lazy_loaders.py b/test/python/utils/test_lazy_loaders.py new file mode 100644 index 000000000000..94db9bb94278 --- /dev/null +++ b/test/python/utils/test_lazy_loaders.py @@ -0,0 +1,349 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the lazy loaders.""" + +import sys +from unittest import mock + +import ddt + +from qiskit.exceptions import MissingOptionalLibraryError +from qiskit.test import QiskitTestCase +from qiskit.utils import LazyImportTester, LazySubprocessTester + +# pylint: disable=no-member + + +def available_importer(**kwargs): + """A LazyImportTester that should succeed.""" + return LazyImportTester("site", **kwargs) + + +def unavailable_importer(**kwargs): + """A LazyImportTester that should fail.""" + return LazyImportTester("_qiskit_this_module_does_not_exist_", **kwargs) + + +def available_process(**kwargs): + """A LazySubprocessTester that should fail.""" + return LazySubprocessTester([sys.executable, "-c", "import sys; sys.exit(0)"], **kwargs) + + +def unavailable_process(**kwargs): + """A LazySubprocessTester that should fail.""" + return LazySubprocessTester([sys.executable, "-c", "import sys; sys.exit(1)"], **kwargs) + + +def mock_availability_test(feature): + """Context manager that mocks out the availability checker for a given dependency checker. The + context manager returns the mocked-out method.""" + # We have to be careful with what we patch because the dependency managers define `__slots__`. + return mock.patch.object(type(feature), "_is_available", wraps=feature._is_available) + + +@ddt.ddt +class TestLazyDependencyTester(QiskitTestCase): + """Tests for the lazy loaders. Within this class, we parameterise the test cases with + generators, rather than the mocks themselves. That allows us to easily generate clean + instances, and means that creation doesn't happen during test collection.""" + + @ddt.data(available_importer, available_process) + def test_evaluates_correctly_true(self, test_generator): + """Test that the available loaders evaluate True in various Boolean contexts.""" + self.assertTrue(test_generator()) + self.assertTrue(bool(test_generator())) + if not test_generator(): + self.fail("did not evaluate true") + + @ddt.data(unavailable_importer, unavailable_process) + def test_evaluates_correctly_false(self, test_generator): + """Test that the available loaders evaluate False in various Boolean contexts.""" + self.assertFalse(test_generator()) + self.assertFalse(bool(test_generator())) + if test_generator(): + self.fail("did not evaluate false") + + @ddt.data(available_importer, available_process, unavailable_importer, unavailable_process) + def test_check_occurs_once(self, test_generator): + """Check that the test of availability is only performed once.""" + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + if feature: + pass + check.assert_called_once() + + if feature: + feature.require_now("no message") + feature.require_in_call(lambda: None)() + feature.require_in_call("no message")(lambda: None)() + feature.require_in_instance(type("Dummy", (), {}))() + feature.require_in_instance("no message")(type("Dummy", (), {}))() + + check.assert_called_once() + + @ddt.data(available_importer, available_process, unavailable_importer, unavailable_process) + def test_callback_occurs_once(self, test_generator): + """Check that the callback is only called once.""" + callback = mock.MagicMock() + + feature = test_generator(callback=callback) + + callback.assert_not_called() + if feature: + pass + callback.assert_called_once_with(bool(feature)) + + callback.reset_mock() + if feature: + feature.require_now("no message") + feature.require_in_call(lambda: None)() + feature.require_in_call("no message")(lambda: None)() + feature.require_in_instance(type("Dummy", (), {}))() + feature.require_in_instance("no message")(type("Dummy", (), {}))() + callback.assert_not_called() + + @ddt.data(available_importer, available_process) + def test_require_now_silently_succeeds_for_available_tests(self, test_generator): + """Test that the available loaders silently do nothing when they are required.""" + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + feature.require_now("no message") + check.assert_called_once() + + @ddt.data(available_importer, available_process) + def test_require_in_call_silently_succeeds_for_available_tests(self, test_generator): + """Test that the available loaders silently do nothing when they are required in the + decorator form.""" + # pylint: disable=function-redefined + + with self.subTest("direct decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_call + def decorated(): + pass + + check.assert_not_called() + decorated() + check.assert_called_once() + + with self.subTest("named decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_call("sentinel name") + def decorated(): + pass + + check.assert_not_called() + decorated() + check.assert_called_once() + + @ddt.data(available_importer, available_process) + def test_require_in_instance_silently_succeeds_for_available_tests(self, test_generator): + """Test that the available loaders silently do nothing when they are required in the + decorator form.""" + # pylint: disable=function-redefined + + with self.subTest("direct decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_instance + class Dummy: + """Dummy class.""" + + check.assert_not_called() + Dummy() + check.assert_called_once() + + with self.subTest("named decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_instance("sentinel name") + class Dummy: + """Dummy class.""" + + check.assert_not_called() + Dummy() + check.assert_called_once() + + @ddt.data(unavailable_importer, unavailable_process) + def test_require_now_raises_for_unavailable_tests(self, test_generator): + """Test that the unavailable loaders loudly raise when they are required.""" + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + with self.assertRaisesRegex(MissingOptionalLibraryError, "sentinel message"): + feature.require_now("sentinel message") + check.assert_called_once() + + @ddt.data(unavailable_importer, unavailable_process) + def test_require_in_call_raises_for_unavailable_tests(self, test_generator): + """Test that the unavailable loaders loudly raise when the inner functions of decorators are + called, and not before, and raise each time they are called.""" + # pylint: disable=function-redefined + + with self.subTest("direct decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_call + def decorated(): + pass + + check.assert_not_called() + with self.assertRaisesRegex(MissingOptionalLibraryError, "decorated"): + decorated() + check.assert_called_once() + with self.assertRaisesRegex(MissingOptionalLibraryError, "decorated"): + decorated() + check.assert_called_once() + + with self.subTest("named decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_call("sentinel message") + def decorated(): + pass + + check.assert_not_called() + with self.assertRaisesRegex(MissingOptionalLibraryError, "sentinel message"): + decorated() + check.assert_called_once() + with self.assertRaisesRegex(MissingOptionalLibraryError, "sentinel message"): + decorated() + check.assert_called_once() + + @ddt.data(unavailable_importer, unavailable_process) + def test_require_in_instance_raises_for_unavailable_tests(self, test_generator): + """Test that the unavailable loaders loudly raise when the inner classes of decorators are + instantiated, and not before, and raise each time they are instantiated.""" + # pylint: disable=function-redefined + + with self.subTest("direct decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_instance + class Dummy: + """Dummy class.""" + + check.assert_not_called() + with self.assertRaisesRegex(MissingOptionalLibraryError, "Dummy"): + Dummy() + check.assert_called_once() + with self.assertRaisesRegex(MissingOptionalLibraryError, "Dummy"): + Dummy() + check.assert_called_once() + + with self.subTest("named decorator"): + feature = test_generator() + with mock_availability_test(feature) as check: + check.assert_not_called() + + @feature.require_in_instance("sentinel message") + class Dummy: + """Dummy class.""" + + check.assert_not_called() + with self.assertRaisesRegex(MissingOptionalLibraryError, "sentinel message"): + Dummy() + check.assert_called_once() + with self.assertRaisesRegex(MissingOptionalLibraryError, "sentinel message"): + Dummy() + check.assert_called_once() + + def test_import_allows_multiple_modules_successful(self): + """Check that the import tester can accept an iterable of modules.""" + # Deliberately using modules that will already be imported to avoid side effects. + feature = LazyImportTester(["site", "sys"]) + with mock_availability_test(feature) as check: + check.assert_not_called() + self.assertTrue(feature) + check.assert_called_once() + + def test_import_allows_multiple_modules_failure(self): + """Check that the import tester can accept an iterable of modules, and will .""" + # Deliberately using modules that will already be imported to avoid side effects. + feature = LazyImportTester(["site", "sys", "_qiskit_module_does_not_exist_"]) + with mock_availability_test(feature) as check: + check.assert_not_called() + self.assertFalse(feature) + check.assert_called_once() + + def test_import_allows_attributes_successful(self): + """Check that the import tester can accept a dictionary mapping module names to attributes, + and that these can be fetched.""" + name_map = { + "_qiskit_dummy_module_1_": ("attr1", "attr2"), + "_qiskit_dummy_module_2_": ("thing1", "thing2"), + } + mock_modules = {} + for module, attributes in name_map.items(): + # We could go through the rigmarole of creating a full module with importlib, but this + # is less complicated and should be sufficient. Property descriptors need to be + # attached to the class to work correctly, and then we provide an instance. + class Module: + """Dummy module.""" + + unaccessed_attribute = mock.PropertyMock() + + for attribute in attributes: + setattr(Module, attribute, mock.PropertyMock()) + mock_modules[module] = Module() + + feature = LazyImportTester(name_map) + with mock.patch.dict(sys.modules, **mock_modules): + self.assertTrue(feature) + + # Retrieve the mocks, and assert that the relevant accesses were made. + for module, attributes in name_map.items(): + mock_module = mock_modules[module] + for attribute in attributes: + vars(type(mock_module))[attribute].assert_called() + vars(type(mock_module))["unaccessed_attribute"].assert_not_called() + + def test_import_allows_attributes_failure(self): + """Check that the import tester can accept a dictionary mapping module names to attributes, + and that these are recognised when they are missing.""" + # We can just use existing modules for this. + name_map = { + "sys": ("executable", "path"), + "builtins": ("list", "_qiskit_dummy_attribute_"), + } + + feature = LazyImportTester(name_map) + self.assertFalse(feature) + + def test_import_fails_with_no_modules(self): + """Catch programmer errors with no modules to test.""" + with self.assertRaises(ValueError): + LazyImportTester([]) + + def test_subprocess_fails_with_no_command(self): + """Catch programmer errors with no command to test.""" + with self.assertRaises(ValueError): + LazySubprocessTester([]) diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py index 296c9325b8a1..e9704135bd06 100644 --- a/test/python/visualization/test_circuit_drawer.py +++ b/test/python/visualization/test_circuit_drawer.py @@ -15,25 +15,27 @@ import unittest import os from unittest.mock import patch -from PIL import Image from qiskit import QuantumCircuit from qiskit.test import QiskitTestCase +from qiskit.utils import optionals from qiskit import visualization from qiskit.visualization import text from qiskit.visualization.exceptions import VisualizationError -if visualization.HAS_MATPLOTLIB: +if optionals.HAS_MATPLOTLIB: from matplotlib import figure +if optionals.HAS_PIL: + from PIL import Image _latex_drawer_condition = unittest.skipUnless( all( ( - visualization.HAS_PYLATEX, - visualization.HAS_PIL, - visualization.HAS_PDFLATEX, - visualization.HAS_PDFTOCAIRO, + optionals.HAS_PYLATEX, + optionals.HAS_PIL, + optionals.HAS_PDFLATEX, + optionals.HAS_PDFTOCAIRO, ) ), "Skipped because not all of PIL, pylatex, pdflatex and pdftocairo are available", @@ -47,9 +49,7 @@ def test_default_output(self): out = visualization.circuit_drawer(circuit) self.assertIsInstance(out, text.TextDrawing) - @unittest.skipUnless( - visualization.HAS_MATPLOTLIB, "Skipped because matplotlib is not available" - ) + @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "Skipped because matplotlib is not available") def test_user_config_default_output(self): with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "mpl"}): circuit = QuantumCircuit() @@ -62,18 +62,14 @@ def test_default_output_with_user_config_not_set(self): out = visualization.circuit_drawer(circuit) self.assertIsInstance(out, text.TextDrawing) - @unittest.skipUnless( - visualization.HAS_MATPLOTLIB, "Skipped because matplotlib is not available" - ) + @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "Skipped because matplotlib is not available") def test_kwarg_priority_over_user_config_default_output(self): with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "latex"}): circuit = QuantumCircuit() out = visualization.circuit_drawer(circuit, output="mpl") self.assertIsInstance(out, figure.Figure) - @unittest.skipUnless( - visualization.HAS_MATPLOTLIB, "Skipped because matplotlib is not available" - ) + @unittest.skipUnless(optionals.HAS_MATPLOTLIB, "Skipped because matplotlib is not available") def test_default_backend_auto_output_with_mpl(self): with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "auto"}): circuit = QuantumCircuit() @@ -82,10 +78,7 @@ def test_default_backend_auto_output_with_mpl(self): def test_default_backend_auto_output_without_mpl(self): with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "auto"}): - with patch.object( - visualization.circuit_visualization, "_matplotlib", autospec=True - ) as mpl_mock: - mpl_mock.HAS_MATPLOTLIB = False + with optionals.HAS_MATPLOTLIB.disable_locally(): circuit = QuantumCircuit() out = visualization.circuit_drawer(circuit) self.assertIsInstance(out, text.TextDrawing) diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index 1f4ec40064e5..c4a6e4e96c15 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -19,12 +19,12 @@ from ddt import ddt, data from qiskit.test.mock import FakeProvider from qiskit.visualization.gate_map import plot_gate_map, plot_coupling_map, plot_circuit_layout -from qiskit.tools.visualization import HAS_MATPLOTLIB +from qiskit.utils import optionals from qiskit import QuantumRegister, QuantumCircuit from qiskit.transpiler import Layout from .visualization import path_to_diagram_reference, QiskitVisualizationTestCase -if HAS_MATPLOTLIB: +if optionals.HAS_MATPLOTLIB: import matplotlib.pyplot as plt @@ -47,7 +47,7 @@ class TestGateMap(QiskitVisualizationTestCase): ) @data(*backends) - @unittest.skipIf(not HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_gate_map(self, backend): """tests plotting of gate map of a device (20 qubit, 16 qubit, 14 qubit and 5 qubit)""" n = backend.configuration().n_qubits @@ -60,7 +60,7 @@ def test_plot_gate_map(self, backend): plt.close(fig) @data(*backends) - @unittest.skipIf(not HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_circuit_layout(self, backend): """tests plot_circuit_layout for each device""" layout_length = int(backend._configuration.n_qubits / 2) @@ -77,7 +77,7 @@ def test_plot_circuit_layout(self, backend): self.assertImagesAreEqual(Image.open(img_buffer), img_ref, 0.1) plt.close(fig) - @unittest.skipIf(not HAS_MATPLOTLIB, "matplotlib not available.") + @unittest.skipIf(not optionals.HAS_MATPLOTLIB, "matplotlib not available.") def test_plot_gate_map_no_backend(self): """tests plotting of gate map without a device""" n_qubits = 8 diff --git a/test/python/visualization/test_pass_manager_drawer.py b/test/python/visualization/test_pass_manager_drawer.py index 2ce1ff4bd95a..fa810a45e5c0 100644 --- a/test/python/visualization/test_pass_manager_drawer.py +++ b/test/python/visualization/test_pass_manager_drawer.py @@ -27,23 +27,10 @@ from qiskit.transpiler.passes import FullAncillaAllocation from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import RemoveResetInZeroState +from qiskit.utils import optionals from .visualization import QiskitVisualizationTestCase, path_to_diagram_reference -try: - import subprocess - - with subprocess.Popen(["dot", "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) as _proc: - _proc.communicate() - if _proc.returncode != 0: - HAS_GRAPHVIZ = False - else: - HAS_GRAPHVIZ = True -except Exception: # pylint: disable=broad-except - # this is raised when the dot command cannot be found, which means GraphViz - # isn't installed - HAS_GRAPHVIZ = False - class TestPassManagerDrawer(QiskitVisualizationTestCase): """Qiskit pass manager drawer tests.""" @@ -68,7 +55,7 @@ def setUp(self): self.pass_manager.append(CXDirection(coupling_map)) self.pass_manager.append(RemoveResetInZeroState()) - @unittest.skipIf(not HAS_GRAPHVIZ, "Graphviz not installed.") + @unittest.skipIf(not optionals.HAS_GRAPHVIZ, "Graphviz not installed.") def test_pass_manager_drawer_basic(self): """Test to see if the drawer draws a normal pass manager correctly""" filename = "current_standard.dot" @@ -77,7 +64,7 @@ def test_pass_manager_drawer_basic(self): self.assertFilesAreEqual(filename, path_to_diagram_reference("pass_manager_standard.dot")) os.remove(filename) - @unittest.skipIf(not HAS_GRAPHVIZ, "Graphviz not installed.") + @unittest.skipIf(not optionals.HAS_GRAPHVIZ, "Graphviz not installed.") def test_pass_manager_drawer_style(self): """Test to see if the colours are updated when provided by the user""" # set colours for some passes, but leave others to take the default values