Skip to content

Commit

Permalink
Optimise import qiskit with lazy imports (Qiskit/qiskit#7525)
Browse files Browse the repository at this point in the history
* Unify lazy handling of optional dependencies

This introduces new `HAS_X` variables for each of Qiskit's optional
dependencies, and provides a simple unified interface to them from
`qiskit.utils.optionals`.  These objects lazily test for their
dependency when evaluated in a Boolean context, and have two `require_`
methods to unify the exception handling.  `require_now` tests
immediately for the dependency and raises `MissingOptionalLibraryError`
if it is not present, and `require_in_call` is a decorator that lazily
tests for the dependencies when the function is called.

These remove the burden of raising nice exceptions from the usage
points; a function marked `HAS_MATPLOTLIB.require_in_call` can now
safely `import matplotlib` without special handling, for example.  This
also provides a unified way for consumers of `qiskit` (such as the test
suite) to query the presence of libraries.

All tests are now lazy, and imports are moved to the point of usage, not
the point of import of the module.  This means that `import qiskit` is
significantly faster for people who have many of the optional
dependencies installed; rather than them all being loaded at initial
import just to test their presence, they will now be loaded on demand.

* Optimise time taken for `import qiskit`

This makes several imports lazy, only being imported when they are
actually called and used.  In particular, no component of `scipy` is
imported during `import qiskit` now, nor is `pkg_resources` (which is
surprisingly heavy).

No changes were made to algorithms or opflow, since these are not
immediately imported during `import qiskit`, and likely require more
significant work than the rest of the library.

* Import missing to-be-deprecated names

* Convert straggler tests to require_now

* Correct requirements in test cases

* Add `require_in_instance` class decorator

Effectively this is just a wrapper around `__init__`, except that this
class-decorator form will do the right thing even if `__init__` isn't
explicitly defined on the given class.

The implementation of `wrap_method` is a replacement for the older
`test.decorators._wrap_method`, which didn't handle all the possible
special cases as well, and messed up the documentation of its wrapped
functions.  That wasn't so important when it was just a private
function, but now it has become public (so that
`test.decorators.enforce_subclasses_call` can still use it from
`qiskit.utils`), it needed reworking to be more polished.

* Privatise non-public names rather than del

* Add tests of `require_in_instance`

* Fix typos in documentation

* Add section on requirements to CONTRIBUTING

* Update documentation on HoareOptimizer error

* Remove UK localisation

* Mention more uses of PIL in documentation

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
jakelishman and mergify[bot] authored Feb 2, 2022
1 parent 526d9fc commit c76d69c
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 102 deletions.
16 changes: 4 additions & 12 deletions qiskit_algorithms/optimizers/bobyqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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),
Expand Down
16 changes: 4 additions & 12 deletions qiskit_algorithms/optimizers/imfil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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,
Expand Down
31 changes: 8 additions & 23 deletions qiskit_algorithms/optimizers/nlopts/nloptimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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
Expand All @@ -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()):
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 6 additions & 24 deletions qiskit_algorithms/optimizers/snobfit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 3 additions & 10 deletions test/optimizers/test_optimizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,7 @@
)
from qiskit.circuit.library import RealAmplitudes
from qiskit.exceptions import MissingOptionalLibraryError
from qiskit.utils import algorithm_globals

try:
import skquant.opt as skq # pylint: disable=unused-import

_HAS_SKQUANT = True
except ImportError:
_HAS_SKQUANT = False
from qiskit.utils import algorithm_globals, optionals


@ddt
Expand Down Expand Up @@ -269,7 +262,7 @@ def test_aqgd(self):
self.assertListEqual(settings["eta"], [0.2, 0.1])
self.assertListEqual(settings["momentum"], [0.25, 0.1])

@unittest.skipIf(not _HAS_SKQUANT, "Install scikit-quant to run this test.")
@unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.")
def test_bobyqa(self):
"""Test BOBYQA is serializable."""

Expand All @@ -278,7 +271,7 @@ def test_bobyqa(self):

self.assertEqual(settings["maxiter"], 200)

@unittest.skipIf(not _HAS_SKQUANT, "Install scikit-quant to run this test.")
@unittest.skipIf(not optionals.HAS_SKQUANT, "Install scikit-quant to run this test.")
def test_imfil(self):
"""Test IMFIL is serializable."""

Expand Down
36 changes: 15 additions & 21 deletions test/test_measure_error_mitigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,25 @@
from qiskit.algorithms.optimizers import SPSA, COBYLA
from qiskit.circuit.library import EfficientSU2
from qiskit.utils.mitigation import CompleteMeasFitter, TensoredMeasFitter
from qiskit.utils import optionals

try:
if optionals.HAS_AER:
# pylint: disable=import-error,no-name-in-module
from qiskit import Aer
from qiskit.providers.aer import noise

HAS_AER = True
except ImportError:
HAS_AER = False

try:
if optionals.HAS_IGNIS:
# pylint: disable=import-error,no-name-in-module
from qiskit.ignis.mitigation.measurement import (
CompleteMeasFitter as CompleteMeasFitter_IG,
TensoredMeasFitter as TensoredMeasFitter_IG,
)

HAS_IGNIS = True
except ImportError:
HAS_IGNIS = False


@ddt
class TestMeasurementErrorMitigation(QiskitAlgorithmsTestCase):
"""Test measurement error mitigation."""

@unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test")
@data("CompleteMeasFitter", "TensoredMeasFitter")
def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str):
"""measurement error mitigation with different qubit order"""
Expand Down Expand Up @@ -110,7 +104,7 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str):

self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3])

@unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test")
@data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]]))
def test_measurement_error_mitigation_with_vqe(self, config):
"""measurement error mitigation test with vqe"""
Expand Down Expand Up @@ -178,7 +172,7 @@ def _get_operator(self, weight_matrix):
opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list]
return PauliSumOp.from_list(opflow_list), shift

@unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test")
def test_measurement_error_mitigation_qaoa(self):
"""measurement error mitigation test with QAOA"""
algorithm_globals.random_seed = 167
Expand Down Expand Up @@ -225,8 +219,8 @@ def test_measurement_error_mitigation_qaoa(self):
result = qaoa.compute_minimum_eigenvalue(operator=qubit_op)
self.assertAlmostEqual(result.eigenvalue.real, ref_eigenvalue, delta=0.05)

@unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(HAS_IGNIS, "qiskit-ignis is required to run this test")
@unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(optionals.HAS_IGNIS, "qiskit-ignis is required to run this test")
@data("CompleteMeasFitter", "TensoredMeasFitter")
def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_str):
"""measurement error mitigation with different qubit order"""
Expand Down Expand Up @@ -287,8 +281,8 @@ def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_s

self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3])

@unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(HAS_IGNIS, "qiskit-ignis is required to run this test")
@unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(optionals.HAS_IGNIS, "qiskit-ignis is required to run this test")
@data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]]))
def test_measurement_error_mitigation_with_vqe_ignis(self, config):
"""measurement error mitigation test with vqe"""
Expand Down Expand Up @@ -330,8 +324,8 @@ def test_measurement_error_mitigation_with_vqe_ignis(self, config):
quantum_instance.reset_execution_results()
self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05)

@unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(HAS_IGNIS, "qiskit-ignis is required to run this test")
@unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(optionals.HAS_IGNIS, "qiskit-ignis is required to run this test")
def test_calibration_results(self):
"""check that results counts are the same with/without error mitigation"""
algorithm_globals.random_seed = 1679
Expand Down Expand Up @@ -367,7 +361,7 @@ def test_calibration_results(self):
counts_array[0], counts_array[1], msg="Counts different with/without fitter."
)

@unittest.skipUnless(HAS_AER, "qiskit-aer is required for this test")
@unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test")
def test_circuit_modified(self):
"""tests that circuits don't get modified on QI execute with error mitigation
as per issue #7449
Expand Down

0 comments on commit c76d69c

Please sign in to comment.