Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from qiskit.exceptions import QiskitError
from qiskit.providers.options import Options
from qiskit.primitives.base import BaseSamplerV2
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_experiments.framework import BackendData, MeasLevel
from qiskit_experiments.framework.store_init_args import StoreInitArgs
from qiskit_experiments.framework.base_analysis import BaseAnalysis
Expand Down Expand Up @@ -380,7 +379,9 @@ def _run_jobs(
if not self._backend_run:
if sampler is None:
# instantiate a sampler from the backend
sampler = Sampler(self.backend)
from qiskit_ibm_runtime import SamplerV2

sampler = SamplerV2(self.backend)

# have to hand set some of these options
# see https://quantum.cloud.ibm.com/docs/api/qiskit-ibm-runtime
Expand Down
13 changes: 8 additions & 5 deletions qiskit_experiments/framework/composite/batch_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@
"""
Batch Experiment class.
"""
from __future__ import annotations

from typing import List, Optional, Dict
from typing import Dict, List, Optional, TYPE_CHECKING
from collections import OrderedDict, defaultdict

from qiskit import QuantumCircuit
from qiskit.providers import Job, Backend, Options
from qiskit_ibm_runtime import SamplerV2 as Sampler

from .composite_experiment import CompositeExperiment, BaseExperiment
from .composite_analysis import CompositeAnalysis

if TYPE_CHECKING:
from qiskit.primitives.base import BaseSamplerV2
from qiskit.providers import Job, Backend, Options


class BatchExperiment(CompositeExperiment):
"""Combine multiple experiments into a batch experiment.
Expand Down Expand Up @@ -138,7 +141,7 @@ def _run_jobs_recursive(
self,
circuits: List[QuantumCircuit],
truncated_metadata: List[Dict],
sampler: Sampler = None,
sampler: BaseSamplerV2 = None,
**run_options,
) -> List[Job]:
# The truncated metadata is a truncation of the original composite metadata.
Expand Down Expand Up @@ -177,7 +180,7 @@ def _run_jobs_recursive(
return jobs

def _run_jobs(
self, circuits: List[QuantumCircuit], sampler: Sampler = None, **run_options
self, circuits: List[QuantumCircuit], sampler: BaseSamplerV2 = None, **run_options
) -> List[Job]:
truncated_metadata = [circ.metadata for circ in circuits]
jobs = self._run_jobs_recursive(circuits, truncated_metadata, sampler, **run_options)
Expand Down
3 changes: 2 additions & 1 deletion qiskit_experiments/library/tomography/fitters/cvxpy_lstsq.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
PreparationBasis,
)
from . import cvxpy_utils
from .cvxpy_utils import cvxpy
from . import lstsq_utils
from .fitter_data import _basis_dimensions

Expand Down Expand Up @@ -136,6 +135,8 @@ def cvxpy_linear_lstsq(
Returns:
The fitted matrix rho that maximizes the least-squares likelihood function.
"""
import cvxpy

t_start = time.time()

if measurement_basis and measurement_qubits is None:
Expand Down
52 changes: 28 additions & 24 deletions qiskit_experiments/library/tomography/fitters/cvxpy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,24 @@
"""
Utility functions for CVXPy module
"""
from __future__ import annotations

from typing import Callable, List, Tuple, Optional, Union
import functools
from typing import Callable, List, TYPE_CHECKING, Tuple, Optional, Union

import numpy as np
import scipy.sparse as sps

from qiskit_experiments.exceptions import AnalysisError

# Check if CVXPY package is installed
try:
import cvxpy
# NOTE: this module gets eagerly imported with `qiskit_experiments/__init__.py`
# so it should not eagerly import cvxpy (a bit expensive to import when not
# needed). Any function outside of this module importing cvxpy or using a
# function from this module should be decorated with `requires_cvxpy`.
if TYPE_CHECKING:
from cvxpy import Problem, Variable
from cvxpy.constraints.constraint import Constraint

HAS_CVXPY = True

except ImportError:
cvxpy = None

HAS_CVXPY = False

# Used for type hints
Problem = None
Variable = None
Constraint = None


def requires_cvxpy(func: Callable) -> Callable:
"""Function decorator for functions requiring CVXPy.
Expand All @@ -49,16 +41,18 @@ def requires_cvxpy(func: Callable) -> Callable:
The decorated function.

Raises:
QiskitError: If CVXPy is not installed.
ImportError: If CVXPy is not installed.
"""

@functools.wraps(func)
def decorated_func(*args, **kwargs):
if not HAS_CVXPY:
try:
import cvxpy # pylint: disable=unused-import
except ImportError as err:
raise ImportError(
f"The CVXPY package is required to for {func}."
f"The CVXPY package is required for {func}."
"You can install it with 'pip install cvxpy'."
)
) from err
return func(*args, **kwargs)

return decorated_func
Expand Down Expand Up @@ -127,6 +121,8 @@ def complex_matrix_variable(
A tuple ``(mat.real, mat.imag, constraints)`` of two real CVXPY
matrix variables, and constraints.
"""
import cvxpy

mat_r = cvxpy.Variable((dim, dim))
mat_i = cvxpy.Variable((dim, dim))
cons = []
Expand Down Expand Up @@ -163,6 +159,8 @@ def psd_constraint(mat_r: Variable, mat_i: Variable) -> List[Constraint]:
Returns:
A list of constraints on the real and imaginary parts.
"""
import cvxpy

bmat = cvxpy.bmat([[mat_r, -mat_i], [mat_i, mat_r]])
return [bmat >> 0]

Expand All @@ -187,9 +185,11 @@ def trace_constraint(
Raises:
TypeError: If input variables are not valid.
"""
import cvxpy

if isinstance(mat_r, (list, tuple)):
arg_r = cvxpy.sum(mat_r)
elif isinstance(mat_r, Variable):
elif isinstance(mat_r, cvxpy.Variable):
arg_r = mat_r
else:
raise TypeError("Input must be a cvxpy variable or list of variables")
Expand All @@ -201,7 +201,7 @@ def trace_constraint(
# If not hermitian add imaginary trace constraint
if isinstance(mat_i, (list, tuple)):
arg_i = cvxpy.sum(mat_i)
elif isinstance(mat_i, Variable):
elif isinstance(mat_i, cvxpy.Variable):
arg_i = mat_i
else:
raise TypeError("Input must be a cvxpy variable or list of variables")
Expand All @@ -228,6 +228,8 @@ def partial_trace_constaint(
Raises:
TypeError: If input variables are not valid.
"""
import cvxpy

sdim = mat_r.shape[0]
output_dim = constraint.shape[0]
input_dim = sdim // output_dim
Expand Down Expand Up @@ -261,10 +263,12 @@ def trace_preserving_constaint(
Raises:
TypeError: If input variables are not valid.
"""
import cvxpy

if isinstance(mat_r, (tuple, list)):
sdim = mat_r[0].shape[0]
arg_r = cvxpy.sum(mat_r)
elif isinstance(mat_r, Variable):
elif isinstance(mat_r, cvxpy.Variable):
sdim = mat_r.shape[0]
arg_r = mat_r
else:
Expand All @@ -282,7 +286,7 @@ def trace_preserving_constaint(
# If not hermitian add imaginary partial trace constraint
if isinstance(mat_i, (tuple, list)):
arg_i = cvxpy.sum(mat_i)
elif isinstance(mat_i, Variable):
elif isinstance(mat_i, cvxpy.Variable):
arg_i = mat_i
else:
raise TypeError("Input must be a cvxpy variable or list of variables")
Expand Down
9 changes: 9 additions & 0 deletions releasenotes/notes/lazy-imports-730268f4cd8763dc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
features:
- |
Importing of optional dependencies has been made delayed until first use.
In particular, ``cvxpy`` is now not imported until a tomography fitter that
uses it is used. Additionally, ``qiskit_ibm_runtime`` is not imported an
experiment is run without being passed a sampler argument. Previously, both
packages were imported when ``qiskit_experiments`` was imported (``cvxpy``
is optional, but it was always imported if it was available).
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,59 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

# pylint: disable=unused-argument, unused-variable
"""Test warning helper."""
"""Test behavior related package dependencies."""

import subprocess
import sys
import textwrap
from test.base import QiskitExperimentsTestCase

from qiskit_experiments.framework import BaseExperiment

class TestOptionalDependencies(QiskitExperimentsTestCase):
"""Test handling of optional dependencies

class TempExperiment(BaseExperiment):
"""Fake experiment."""
Note: these tests use subprocesses in order to test import handling. That
is an expensive operation compared to running code within a process that
has already imported everything (importing qiskit_experiments takes about 3
seconds), so the amount of this kind of test should be kept to a minimum.
"""

def test_no_optional_dependencies(self):
"""Test that optional dependencies not imported by 'import qiskit_experiments'"""
script = """
import sys

import qiskit_experiments

def __init__(self, physical_qubits):
super().__init__(physical_qubits)

def circuits(self):
pass
top_level_modules = {m.partition(".")[0] for m in sys.modules}

optional_deps = [
"cvxpy",
"qiskit_aer",
"qiskit_ibm_runtime",
"sklearn",
]

unexpected = [d for d in optional_deps if d in top_level_modules]
if unexpected:
print(", ".join(unexpected))
"""
script = textwrap.dedent(script)

proc = subprocess.run(
[sys.executable, "-c", script], check=False, text=True, capture_output=True
)

class TestWarningsHelper(QiskitExperimentsTestCase):
"""Test case for warnings decorator with tricky behavior."""
self.assertTrue(
proc.stdout == "",
msg=f"Unexpected dependency imports: {proc.stdout}",
)
self.assertEqual(
proc.returncode,
0,
msg=f"Test script failed:\n{proc.stderr}",
)

def test_warn_sklearn(self):
"""Test that a suggestion to import scikit-learn is given when appropriate"""
Expand Down