diff --git a/docs/_ext/custom_styles/example/example_experiment.py b/docs/_ext/custom_styles/example/example_experiment.py index 6c42e7e593..75648e86ca 100644 --- a/docs/_ext/custom_styles/example/example_experiment.py +++ b/docs/_ext/custom_styles/example/example_experiment.py @@ -212,5 +212,5 @@ def __init__(self, qubit: int): """ super().__init__(qubits=[qubit]) - def circuits(self, backend=None): + def circuits(self): pass diff --git a/qiskit_experiments/calibration_management/base_calibration_experiment.py b/qiskit_experiments/calibration_management/base_calibration_experiment.py index d46506aff6..899f83da58 100644 --- a/qiskit_experiments/calibration_management/base_calibration_experiment.py +++ b/qiskit_experiments/calibration_management/base_calibration_experiment.py @@ -339,9 +339,9 @@ def get_schedule( return schedules - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """A wrapper to introduce an optional hook to add circuit metadata.""" - circuits = super().circuits(backend) + circuits = super().circuits() self._add_cal_metadata(circuits) @@ -358,14 +358,16 @@ def _add_cal_metadata(self, circuits: List[QuantumCircuit]): def run( self, - backend: Backend, + backend: Optional[Backend] = None, analysis: bool = True, **run_options, ) -> ExperimentData: """Run an experiment, perform analysis, and update any calibrations. Args: - backend: The backend to run the experiment on. + backend: Optional, the backend to run the experiment on. This + will override any currently set backends for the single + execution. analysis: If True run analysis on the experiment data. run_options: backend runtime options used for circuit execution. diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 47815f98a3..f60a170081 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -14,15 +14,14 @@ """ from abc import ABC, abstractmethod -from typing import Iterable, Optional, Tuple, List, Dict import copy from numbers import Integral +from typing import Sequence, Optional, Tuple, List, Dict, Union from qiskit import transpile, assemble, QuantumCircuit from qiskit.providers import BaseJob from qiskit.providers.backend import Backend from qiskit.providers.basebackend import BaseBackend as LegacyBackend -from qiskit.test.mock import FakeBackend from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel from qiskit_experiments.framework import Options @@ -47,12 +46,18 @@ class BaseExperiment(ABC): # ExperimentData class for experiment __experiment_data__ = ExperimentData - def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None): + def __init__( + self, + qubits: Sequence[int], + backend: Optional[Backend] = None, + experiment_type: Optional[str] = None, + ): """Initialize the experiment object. Args: qubits: the number of qubits or list of physical qubits for the experiment. + backend: Optional, the backend to run the experiment on. experiment_type: Optional, the experiment type string. Raises: @@ -61,6 +66,11 @@ def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None) # Experiment identification metadata self._type = experiment_type if experiment_type else type(self).__name__ + # Backend + self._backend = None + if backend is not None: + self._set_backend(backend) + # Circuit parameters if isinstance(qubits, Integral): self._num_qubits = qubits @@ -77,16 +87,63 @@ def __init__(self, qubits: Iterable[int], experiment_type: Optional[str] = None) self._run_options = self._default_run_options() self._analysis_options = self._default_analysis_options() + @property + def experiment_type(self) -> str: + """Return experiment type.""" + return self._type + + @property + def physical_qubits(self) -> Tuple[int, ...]: + """Return the device qubits for the experiment.""" + return self._physical_qubits + + @property + def num_qubits(self) -> int: + """Return the number of qubits for the experiment.""" + return self._num_qubits + + @property + def backend(self) -> Union[Backend, None]: + """Return the backend for the experiment""" + return self._backend + + @backend.setter + def backend(self, backend: Union[Backend, None]) -> None: + """Set the backend for the experiment""" + self._set_backend(backend) + + def _set_backend(self, backend: Backend): + """Set the backend for the experiment. + + Subclasses can override this method to extract additional + properties from the supplied backend if required. + """ + self._backend = backend + + def copy(self) -> "BaseExperiment": + """Return a copy of the experiment""" + # We want to avoid a deep copy be default for performance so we + # need to also copy the Options structures so that if they are + # updated on the copy they don't effect the original. + ret = copy.copy(self) + ret._experiment_options = copy.copy(self._experiment_options) + ret._run_options = copy.copy(self._run_options) + ret._transpile_options = copy.copy(self._transpile_options) + ret._analysis_options = copy.copy(self._analysis_options) + return ret + def run( self, - backend: Backend, + backend: Optional[Backend] = None, analysis: bool = True, **run_options, ) -> ExperimentData: """Run an experiment and perform analysis. Args: - backend: The backend to run the experiment on. + backend: Optional, the backend to run the experiment on. This + will override any currently set backends for the single + execution. analysis: If True run analysis on the experiment data. run_options: backend runtime options used for circuit execution. @@ -97,70 +154,43 @@ def run( QiskitError: if experiment is run with an incompatible existing ExperimentData container. """ - # Create experiment data container - experiment_data = self._initialize_experiment_data(backend) + if backend is None: + experiment = self + else: + experiment = self.copy() + experiment._set_backend(backend) + if experiment.backend is None: + raise QiskitError("Cannot run experiment, no backend has been set.") + + # Initialize result container + experiment_data = experiment._initialize_experiment_data() # Run options - run_opts = copy.copy(self.run_options) + run_opts = copy.copy(experiment.run_options) run_opts.update_options(**run_options) run_opts = run_opts.__dict__ - # Scheduling parameters - if backend.configuration().simulator is False and isinstance(backend, FakeBackend) is False: - timing_constraints = getattr(self.transpile_options.__dict__, "timing_constraints", {}) - timing_constraints["acquire_alignment"] = getattr( - timing_constraints, "acquire_alignment", 16 - ) - scheduling_method = getattr( - self.transpile_options.__dict__, "scheduling_method", "alap" - ) - self.set_transpile_options( - timing_constraints=timing_constraints, scheduling_method=scheduling_method - ) - # Generate and transpile circuits - transpile_opts = copy.copy(self.transpile_options.__dict__) - transpile_opts["initial_layout"] = list(self._physical_qubits) - circuits = transpile(self.circuits(backend), backend, **transpile_opts) - self._postprocess_transpiled_circuits(circuits, backend, **run_options) - - # Run experiment jobs - max_experiments = getattr(backend.configuration(), "max_experiments", None) - if max_experiments and len(circuits) > max_experiments: - # Split jobs for backends that have a maximum job size - job_circuits = [ - circuits[i : i + max_experiments] for i in range(0, len(circuits), max_experiments) - ] - else: - # Run as single job - job_circuits = [circuits] + transpile_opts = copy.copy(experiment.transpile_options.__dict__) + transpile_opts["initial_layout"] = list(experiment.physical_qubits) + circuits = transpile(experiment.circuits(), experiment.backend, **transpile_opts) + experiment._postprocess_transpiled_circuits(circuits, **run_options) # Run jobs - jobs = [] - for circs in job_circuits: - if isinstance(backend, LegacyBackend): - qobj = assemble(circs, backend=backend, **run_opts) - job = backend.run(qobj) - else: - job = backend.run(circs, **run_opts) - jobs.append(job) - - # Add experiment option metadata - self._add_job_metadata(experiment_data, jobs, **run_opts) - - # Add jobs + jobs = experiment._run_jobs(circuits, **run_opts) experiment_data.add_data(jobs) + experiment._add_job_metadata(experiment_data, jobs, **run_opts) # Optionally run analysis - if analysis and self.__analysis_class__: + if analysis and self.__analysis_class__ is not None: experiment_data.add_analysis_callback(self.run_analysis) # Return the ExperimentData future return experiment_data - def _initialize_experiment_data(self, backend: Backend) -> ExperimentData: + def _initialize_experiment_data(self) -> ExperimentData: """Initialize the return data container for the experiment run""" - return self.__experiment_data__(experiment=self, backend=backend) + return self.__experiment_data__(experiment=self) def run_analysis(self, experiment_data: ExperimentData, **options) -> ExperimentData: """Run analysis and update ExperimentData with analysis result. @@ -187,20 +217,29 @@ def run_analysis(self, experiment_data: ExperimentData, **options) -> Experiment analysis.run(experiment_data, **analysis_options) return experiment_data - @property - def num_qubits(self) -> int: - """Return the number of qubits for this experiment.""" - return self._num_qubits - - @property - def physical_qubits(self) -> Tuple[int]: - """Return the physical qubits for this experiment.""" - return self._physical_qubits + def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[BaseJob]: + """Run circuits on backend as 1 or more jobs.""" + # Run experiment jobs + max_experiments = getattr(self.backend.configuration(), "max_experiments", None) + if max_experiments and len(circuits) > max_experiments: + # Split jobs for backends that have a maximum job size + job_circuits = [ + circuits[i : i + max_experiments] for i in range(0, len(circuits), max_experiments) + ] + else: + # Run as single job + job_circuits = [circuits] - @property - def experiment_type(self) -> str: - """Return experiment type.""" - return self._type + # Run jobs + jobs = [] + for circs in job_circuits: + if isinstance(self.backend, LegacyBackend): + qobj = assemble(circs, backend=self.backend, **run_options) + job = self.backend.run(qobj) + else: + job = self.backend.run(circs, **run_options) + jobs.append(job) + return jobs @classmethod def analysis(cls): @@ -211,12 +250,9 @@ def analysis(cls): return cls.__analysis_class__() @abstractmethod - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits. - Args: - backend: Optional, a backend object. - Returns: A list of :class:`QuantumCircuit`. @@ -330,7 +366,7 @@ def set_analysis_options(self, **fields): """ self._analysis_options.update_options(**fields) - def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): + def _postprocess_transpiled_circuits(self, circuits: List[QuantumCircuit], **run_options): """Additional post-processing of transpiled circuits before running on backend""" pass diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index c6d0a71ada..67ee527a09 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -13,21 +13,23 @@ Batch Experiment class. """ +from typing import List, Optional from collections import OrderedDict from qiskit import QuantumCircuit - -from .composite_experiment import CompositeExperiment +from qiskit.providers.backend import Backend +from .composite_experiment import CompositeExperiment, BaseExperiment class BatchExperiment(CompositeExperiment): """Batch experiment class""" - def __init__(self, experiments): + def __init__(self, experiments: List[BaseExperiment], backend: Optional[Backend] = None): """Initialize a batch experiment. Args: - experiments (List[BaseExperiment]): a list of experiments. + experiments: a list of experiments. + backend: Optional, the backend to run the experiment on. """ # Generate qubit map @@ -39,9 +41,9 @@ def __init__(self, experiments): self._qubit_map[physical_qubit] = logical_qubit logical_qubit += 1 qubits = tuple(self._qubit_map.keys()) - super().__init__(experiments, qubits) + super().__init__(experiments, qubits, backend=backend) - def circuits(self, backend=None): + def circuits(self): batch_circuits = [] @@ -51,7 +53,7 @@ def circuits(self, backend=None): qubit_mapping = None else: qubit_mapping = [self._qubit_map[qubit] for qubit in expr.physical_qubits] - for circuit in expr.circuits(backend): + for circuit in expr.circuits(): # Update metadata circuit.metadata = { "experiment_type": self._type, diff --git a/qiskit_experiments/framework/composite/composite_experiment.py b/qiskit_experiments/framework/composite/composite_experiment.py index 5c41d0e0f2..484286aade 100644 --- a/qiskit_experiments/framework/composite/composite_experiment.py +++ b/qiskit_experiments/framework/composite/composite_experiment.py @@ -13,9 +13,11 @@ Composite Experiment abstract base class. """ +from typing import List, Sequence, Optional from abc import abstractmethod import warnings +from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment from .composite_experiment_data import CompositeExperimentData from .composite_analysis import CompositeAnalysis @@ -27,21 +29,27 @@ class CompositeExperiment(BaseExperiment): __analysis_class__ = CompositeAnalysis __experiment_data__ = CompositeExperimentData - def __init__(self, experiments, qubits, experiment_type=None): + def __init__( + self, + experiments: List[BaseExperiment], + qubits: Sequence[int], + backend: Optional[Backend] = None, + experiment_type: Optional[str] = None, + ): """Initialize the composite experiment object. Args: - experiments (List[BaseExperiment]): a list of experiment objects. - qubits (int or Iterable[int]): the number of qubits or list of - physical qubits for the experiment. - experiment_type (str): Optional, composite experiment subclass name. + experiments: a list of experiment objects. + qubits: the number of qubits or list of physical qubits for the experiment. + backend: Optional, the backend to run the experiment on. + experiment_type: Optional, composite experiment subclass name. """ self._experiments = experiments self._num_experiments = len(experiments) - super().__init__(qubits, experiment_type=experiment_type) + super().__init__(qubits, backend=backend, experiment_type=experiment_type) @abstractmethod - def circuits(self, backend=None): + def circuits(self): pass @property @@ -64,6 +72,18 @@ def component_analysis(self, index): """Return the component experiment Analysis object""" return self.component_experiment(index).analysis() + def copy(self) -> "BaseExperiment": + """Return a copy of the experiment""" + ret = super().copy() + # Recursively call copy of component experiments + ret._experiments = [exp.copy() for exp in self._experiments] + return ret + + def _set_backend(self, backend): + super()._set_backend(backend) + for subexp in self._experiments: + subexp._set_backend(backend) + def _add_job_metadata(self, experiment_data, jobs, **run_options): # Add composite metadata super()._add_job_metadata(experiment_data, jobs, **run_options) @@ -85,7 +105,7 @@ def _add_job_metadata(self, experiment_data, jobs, **run_options): sub_data = experiment_data.component_experiment_data(i) sub_exp._add_job_metadata(sub_data, jobs, **run_options) - def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): + def _postprocess_transpiled_circuits(self, circuits, **run_options): for expr in self._experiments: if not isinstance(expr, CompositeExperiment): - expr._postprocess_transpiled_circuits(circuits, backend, **run_options) + expr._postprocess_transpiled_circuits(circuits, **run_options) diff --git a/qiskit_experiments/framework/composite/parallel_experiment.py b/qiskit_experiments/framework/composite/parallel_experiment.py index 286b6c8efc..4643b3f0e6 100644 --- a/qiskit_experiments/framework/composite/parallel_experiment.py +++ b/qiskit_experiments/framework/composite/parallel_experiment.py @@ -12,27 +12,29 @@ """ Parallel Experiment class. """ +from typing import List, Optional from qiskit import QuantumCircuit, ClassicalRegister - -from .composite_experiment import CompositeExperiment +from qiskit.providers.backend import Backend +from .composite_experiment import CompositeExperiment, BaseExperiment class ParallelExperiment(CompositeExperiment): """Parallel Experiment class""" - def __init__(self, experiments): + def __init__(self, experiments: List[BaseExperiment], backend: Optional[Backend] = None): """Initialize the analysis object. Args: - experiments (List[BaseExperiment]): a list of experiments. + experiments: a list of experiments. + backend: Optional, the backend to run the experiment on. """ qubits = [] for exp in experiments: qubits += exp.physical_qubits - super().__init__(experiments, qubits) + super().__init__(experiments, qubits, backend=backend) - def circuits(self, backend=None): + def circuits(self): sub_circuits = [] sub_qubits = [] @@ -42,7 +44,7 @@ def circuits(self, backend=None): # Generate data for combination for expr in self._experiments: # Add subcircuits - circs = expr.circuits(backend) + circs = expr.circuits() sub_circuits.append(circs) sub_size.append(len(circs)) diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index eae48238bb..225a932644 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -35,9 +35,15 @@ def __init__(self, experiment=None, backend=None, parent_id=None, job_ids=None): in the setting of a composite experiment job_ids (list[str]): Optional, IDs of jobs submitted for the experiment. """ + if experiment is not None: + backend = backend or experiment.backend + experiment_type = experiment.experiment_type + else: + experiment_type = None + self._experiment = experiment super().__init__( - experiment_type=experiment.experiment_type if experiment else None, + experiment_type=experiment_type, backend=backend, parent_id=parent_id, job_ids=job_ids, @@ -46,7 +52,7 @@ def __init__(self, experiment=None, backend=None, parent_id=None, job_ids=None): @property def experiment(self): - """Return Experiment object. + """Return the experiment for this data. Returns: BaseExperiment: the experiment object. @@ -98,11 +104,10 @@ def _copy_metadata(self, new_instance: Optional["ExperimentData"] = None) -> "Ex return super()._copy_metadata(new_instance) def __repr__(self): - out = f"{type(self).__name__}({self.experiment_type}" - out += f", {self.experiment_id}" - if self.backend: - out += f", backend={self.backend}" - if self.job_ids: - out += f", job_ids={self.job_ids}" - out += ")" + out = ( + f"" + ) return out diff --git a/qiskit_experiments/library/calibration/drag.py b/qiskit_experiments/library/calibration/drag.py index f939abef74..055c4b9e10 100644 --- a/qiskit_experiments/library/calibration/drag.py +++ b/qiskit_experiments/library/calibration/drag.py @@ -17,7 +17,7 @@ from qiskit import QuantumCircuit from qiskit.circuit import Gate, Parameter -from qiskit.providers import Backend +from qiskit.providers.backend import Backend import qiskit.pulse as pulse from qiskit_experiments.framework import BaseExperiment, Options @@ -132,20 +132,18 @@ def set_experiment_options(self, reps: Optional[List] = None, **fields): super().set_experiment_options(reps=reps, **fields) - def __init__(self, qubit: int): + def __init__(self, qubit: int, backend: Optional[Backend] = None): """ Args: qubit: The qubit for which to run the Drag calibration. + backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit]) + super().__init__([qubit], backend=backend) - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Create the circuits for the Drag calibration. - Args: - backend: A backend object. - Returns: circuits: The circuits that will run the Drag calibration. @@ -159,7 +157,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: if schedule is None: beta = Parameter("β") - with pulse.build(backend=backend, name="drag") as schedule: + with pulse.build(backend=self.backend, name="drag") as schedule: pulse.play( pulse.Drag( duration=self.experiment_options.duration, @@ -167,7 +165,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: sigma=self.experiment_options.sigma, beta=beta, ), - pulse.DriveChannel(self._physical_qubits[0]), + pulse.DriveChannel(self.physical_qubits[0]), ) if len(schedule.parameters) != 1: diff --git a/qiskit_experiments/library/calibration/fine_amplitude.py b/qiskit_experiments/library/calibration/fine_amplitude.py index 57dc010a44..63538736df 100644 --- a/qiskit_experiments/library/calibration/fine_amplitude.py +++ b/qiskit_experiments/library/calibration/fine_amplitude.py @@ -16,6 +16,7 @@ import numpy as np from qiskit.circuit import Gate, QuantumCircuit +from qiskit.providers.backend import Backend from qiskit_experiments.calibration_management import ( BaseCalibrationExperiment, @@ -41,6 +42,7 @@ def __init__( qubit: int, calibrations: BackendCalibrations, schedule_name: str, + backend: Optional[Backend] = None, cal_parameter_name: Optional[str] = "amp", auto_update: bool = True, ): @@ -50,16 +52,18 @@ def __init__( qubit: The qubit for which to run the fine amplitude calibration. calibrations: The calibrations instance with the schedules. schedule_name: The name of the schedule to calibrate. + backend: Optional, the backend to run the experiment on. cal_parameter_name: The name of the parameter in the schedule to update. auto_update: Whether or not to automatically update the calibrations. By default this variable is set to True. - + on. """ super().__init__( calibrations, qubit, Gate(name=schedule_name, num_qubits=1, params=[]), schedule_name=schedule_name, + backend=backend, cal_parameter_name=cal_parameter_name, auto_update=auto_update, ) @@ -99,7 +103,7 @@ def _add_cal_metadata(self, circuits: List[QuantumCircuit]): param_val = self._cals.get_parameter_value( self._param_name, - self._physical_qubits, + self.physical_qubits, self._sched_name, group=self.experiment_options.group, ) diff --git a/qiskit_experiments/library/calibration/fine_drag.py b/qiskit_experiments/library/calibration/fine_drag.py index d8de4f72df..f8a6b6c82c 100644 --- a/qiskit_experiments/library/calibration/fine_drag.py +++ b/qiskit_experiments/library/calibration/fine_drag.py @@ -12,13 +12,13 @@ """Fine DRAG calibration experiment.""" -from typing import Optional, List +from typing import List, Optional import numpy as np from qiskit import QuantumCircuit from qiskit.circuit import Gate from qiskit.circuit.library import XGate, SXGate -from qiskit.providers import Backend +from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.library.calibration.analysis.fine_drag_analysis import ( @@ -165,13 +165,14 @@ def _default_analysis_options(cls) -> Options: return options - def __init__(self, qubit: int): + def __init__(self, qubit: int, backend: Optional[Backend] = None): """Setup a fine amplitude experiment on the given qubit. Args: qubit: The qubit on which to run the fine amplitude calibration experiment. + backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit]) + super().__init__([qubit], backend=backend) @staticmethod def _pre_circuit() -> QuantumCircuit: @@ -185,12 +186,9 @@ def _post_circuit() -> QuantumCircuit: circ.ry(np.pi / 2, 0) # Maps unwanted Z rotations to qubit population. return circ - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Create the circuits for the fine DRAG calibration experiment. - Args: - backend: A backend object. - Returns: A list of circuits with a variable number of gates. Each gate has the same pulse schedule. @@ -216,7 +214,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: circuit.measure_all() if schedule is not None: - circuit.add_calibration(schedule.name, self._physical_qubits, schedule, params=[]) + circuit.add_calibration(schedule.name, self.physical_qubits, schedule, params=[]) circuit.metadata = { "experiment_type": self._type, diff --git a/qiskit_experiments/library/calibration/rabi.py b/qiskit_experiments/library/calibration/rabi.py index a85998ae1c..fcb97b7aea 100644 --- a/qiskit_experiments/library/calibration/rabi.py +++ b/qiskit_experiments/library/calibration/rabi.py @@ -104,7 +104,7 @@ def _default_analysis_options(cls) -> Options: return options - def __init__(self, qubit: int): + def __init__(self, qubit: int, backend: Optional[Backend] = None): """Initialize a Rabi experiment on the given qubit. The parameters of the Gaussian Rabi pulse can be specified at run-time. @@ -116,8 +116,9 @@ def __init__(self, qubit: int): Args: qubit: The qubit on which to run the Rabi experiment. + backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit]) + super().__init__([qubit], backend=backend) def _template_circuit(self, amp_param) -> QuantumCircuit: """Return the template quantum circuit.""" @@ -144,12 +145,9 @@ def _default_gate_schedule(self, backend: Optional[Backend] = None): return default_schedule - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Create the circuits for the Rabi experiment. - Args: - backend: A backend object. - Returns: A list of circuits with a rabi gate with an attached schedule. Each schedule will have a different value of the scanned amplitude. @@ -163,7 +161,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: schedule = self.experiment_options.get("schedule", None) if schedule is None: - schedule = self._default_gate_schedule(backend=backend) + schedule = self._default_gate_schedule(backend=self.backend) else: if self.physical_qubits[0] not in set(ch.index for ch in schedule.channels): raise CalibrationError( @@ -182,6 +180,12 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: self.__rabi_gate_name__, (self.physical_qubits[0],), schedule, params=[param] ) + # Get backend dt + if self.backend is not None: + backend_dt = getattr(self.backend.configuration(), "dt", "n.a.") + else: + backend_dt = "n.a" + # Create the circuits to run circs = [] for amp in self.experiment_options.amplitudes: @@ -194,11 +198,9 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: "unit": "arb. unit", "amplitude": amp, "schedule": str(schedule), + "dt": backend_dt, } - if backend: - assigned_circ.metadata["dt"] = getattr(backend.configuration(), "dt", "n.a.") - circs.append(assigned_circ) return circs diff --git a/qiskit_experiments/library/calibration/ramsey_xy.py b/qiskit_experiments/library/calibration/ramsey_xy.py index 7fa7d5d5ec..3411a0f5a8 100644 --- a/qiskit_experiments/library/calibration/ramsey_xy.py +++ b/qiskit_experiments/library/calibration/ramsey_xy.py @@ -17,8 +17,8 @@ from qiskit import QuantumCircuit from qiskit.circuit import Parameter -from qiskit.providers import Backend from qiskit.utils import apply_prefix +from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment from qiskit_experiments.library.calibration.analysis.remsey_xy_analysis import RamseyXYAnalysis @@ -106,6 +106,7 @@ def _default_experiment_options(cls): def __init__( self, qubit: int, + backend: Optional[Backend] = None, delays: Optional[List] = None, unit: str = "s", osc_freq: float = 2e6, @@ -114,12 +115,13 @@ def __init__( Args: qubit: The qubit on which to run the Ramsey XY experiment. + backend: Optional, the backend to run the experiment on. delays: The delays to scan. unit: The unit of the delays. osc_freq: the oscillation frequency induced by the user through a virtual Rz rotation. This quantity is given in Hz. """ - super().__init__([qubit]) + super().__init__([qubit], backend=backend) delays = delays or self.experiment_options.delays self.set_experiment_options(delays=delays, unit=unit, osc_freq=osc_freq) @@ -132,12 +134,9 @@ def _pre_circuit(self) -> QuantumCircuit: """ return QuantumCircuit(1) - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Create the circuits for the Ramsey XY calibration experiment. - Args: - backend: A backend object. - Returns: A list of circuits with a variable delay. @@ -149,7 +148,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: conversion_factor = 1 if self.experiment_options.unit == "dt": try: - conversion_factor = getattr(backend.configuration(), "dt") + conversion_factor = getattr(self.backend.configuration(), "dt") except AttributeError as no_dt: raise AttributeError( "Dt parameter is missing from the backend's configuration." diff --git a/qiskit_experiments/library/calibration/rough_frequency.py b/qiskit_experiments/library/calibration/rough_frequency.py index 5088ba0c7f..bc5dc1a77c 100644 --- a/qiskit_experiments/library/calibration/rough_frequency.py +++ b/qiskit_experiments/library/calibration/rough_frequency.py @@ -12,7 +12,8 @@ """Calibration version of spectroscopy experiments.""" -from typing import Iterable +from typing import Iterable, Optional +from qiskit.providers.backend import Backend from qiskit_experiments.library.characterization.qubit_spectroscopy import QubitSpectroscopy from qiskit_experiments.library.characterization.ef_spectroscopy import EFSpectroscopy @@ -31,6 +32,7 @@ def __init__( qubit: int, calibrations: BackendCalibrations, frequencies: Iterable[float], + backend: Optional[Backend] = None, unit: str = "Hz", auto_update: bool = True, absolute: bool = True, @@ -42,6 +44,7 @@ def __init__( calibrations: If calibrations is given then running the experiment may update the values of the frequencies stored in calibrations. frequencies: The frequencies to scan in the experiment. + backend: Optional, the backend to run the experiment on. unit: The unit in which the user specifies the frequencies. Can be one of 'Hz', 'kHz', 'MHz', 'GHz'. Internally, all frequencies will be converted to 'Hz'. auto_update: If set to True, which is the default, then the experiment will @@ -57,8 +60,9 @@ def __init__( calibrations, qubit, frequencies, - unit, - absolute, + backend=backend, + unit=unit, + absolute=absolute, updater=Frequency, auto_update=auto_update, ) diff --git a/qiskit_experiments/library/characterization/cr_hamiltonian.py b/qiskit_experiments/library/characterization/cr_hamiltonian.py index 9d44ac925d..ccb640e67d 100644 --- a/qiskit_experiments/library/characterization/cr_hamiltonian.py +++ b/qiskit_experiments/library/characterization/cr_hamiltonian.py @@ -13,7 +13,7 @@ Cross resonance Hamiltonian tomography. """ -from typing import List, Tuple, Optional, Iterable, Dict +from typing import List, Tuple, Iterable, Dict, Optional import numpy as np from qiskit import pulse, circuit, QuantumCircuit @@ -130,6 +130,7 @@ def __init__( self, qubits: Tuple[int, int], flat_top_widths: Iterable[float], + backend: Optional[Backend] = None, unit: str = "dt", **kwargs, ): @@ -141,13 +142,14 @@ def __init__( flat_top_widths: The total duration of the square part of cross resonance pulse(s) to scan. The total pulse duration including Gaussian rising and falling edges is implicitly computed with experiment parameters ``sigma`` and ``risefall``. + backend: Optional, the backend to run the experiment on. unit: The time unit of durations. kwargs: Pulse parameters. See :meth:`experiment_options` for details. Raises: QiskitError: When ``qubits`` length is not 2. """ - super().__init__(qubits=qubits) + super().__init__(qubits, backend=backend) if len(qubits) != 2: raise QiskitError( @@ -252,12 +254,9 @@ def _build_cr_schedule( return cross_resonance - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits. - Args: - backend: The target backend. - Returns: A list of :class:`QuantumCircuit`. @@ -268,7 +267,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: prefactor = 1.0 try: - dt_factor = backend.configuration().dt + dt_factor = self.backend.configuration().dt except AttributeError as ex: raise AttributeError("Backend configuration does not provide time resolution.") from ex @@ -331,7 +330,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: gate=cr_gate, qubits=self.physical_qubits, schedule=self._build_cr_schedule( - backend=backend, + backend=self.backend, flat_top_width=prefactor * flat_top_width / self.__n_cr_pulses__, sigma=prefactor * opt.sigma, ), diff --git a/qiskit_experiments/library/characterization/fine_amplitude.py b/qiskit_experiments/library/characterization/fine_amplitude.py index 6f42b02762..3c4c4c5fa2 100644 --- a/qiskit_experiments/library/characterization/fine_amplitude.py +++ b/qiskit_experiments/library/characterization/fine_amplitude.py @@ -18,8 +18,7 @@ from qiskit import QuantumCircuit from qiskit.circuit import Gate from qiskit.circuit.library import XGate, SXGate -from qiskit.providers import Backend - +from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.library.calibration.analysis.fine_amplitude_analysis import ( FineAmplitudeAnalysis, @@ -126,15 +125,16 @@ def _default_analysis_options(cls) -> Options: return options - def __init__(self, qubit: int, gate: Gate): + def __init__(self, qubit: int, gate: Gate, backend: Optional[Backend] = None): """Setup a fine amplitude experiment on the given qubit. Args: qubit: The qubit on which to run the fine amplitude calibration experiment. gate: The gate that will be repeated. + backend: Optional, the backend to run the experiment on. """ - super().__init__([qubit]) - self.experiment_options.gate = gate + super().__init__([qubit], backend=backend) + self.set_experiment_options(gate=gate) def _pre_circuit(self) -> QuantumCircuit: """Return a preparation circuit. @@ -149,12 +149,9 @@ def _pre_circuit(self) -> QuantumCircuit: return circuit - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Create the circuits for the fine amplitude calibration experiment. - Args: - backend: A backend object. - Returns: A list of circuits with a variable number of gates. diff --git a/qiskit_experiments/library/characterization/qubit_spectroscopy.py b/qiskit_experiments/library/characterization/qubit_spectroscopy.py index d44b3038d3..09314d0131 100644 --- a/qiskit_experiments/library/characterization/qubit_spectroscopy.py +++ b/qiskit_experiments/library/characterization/qubit_spectroscopy.py @@ -95,6 +95,7 @@ def __init__( self, qubit: int, frequencies: Iterable[float], + backend: Optional[Backend] = None, unit: str = "Hz", absolute: bool = True, ): @@ -110,6 +111,7 @@ def __init__( Args: qubit: The qubit on which to run spectroscopy. frequencies: The frequencies to scan in the experiment. + backend: Optional, the backend to run the experiment on. unit: The unit in which the user specifies the frequencies. Can be one of 'Hz', 'kHz', 'MHz', 'GHz'. Internally, all frequencies will be converted to 'Hz'. absolute: Boolean to specify if the frequencies are absolute or relative to the @@ -119,7 +121,7 @@ def __init__( QiskitError: if there are less than three frequency shifts or if the unit is not known. """ - super().__init__([qubit]) + super().__init__([qubit], backend=backend) if len(frequencies) < 3: raise QiskitError("Spectroscopy requires at least three frequencies.") @@ -166,15 +168,12 @@ def _template_circuit(self, freq_param) -> QuantumCircuit: return circuit - def circuits(self, backend: Optional[Backend] = None): + def circuits(self): """Create the circuit for the spectroscopy experiment. The circuits are based on a GaussianSquare pulse and a frequency_shift instruction encapsulated in a gate. - Args: - backend: A backend object. - Returns: circuits: The circuits that will run the spectroscopy experiment. @@ -182,24 +181,34 @@ def circuits(self, backend: Optional[Backend] = None): QiskitError: - If absolute frequencies are used but no backend is given. - If the backend configuration does not define dt. + AttributeError: If backend to run on does not contain 'dt' configuration. """ - if backend is None and self._absolute: + if self.backend is None and self._absolute: raise QiskitError("Cannot run spectroscopy absolute to qubit without a backend.") # Create a template circuit - sched, freq_param = self._spec_gate_schedule(backend) + sched, freq_param = self._spec_gate_schedule(self.backend) circuit = self._template_circuit(freq_param) circuit.add_calibration("Spec", (self.physical_qubits[0],), sched, params=[freq_param]) + # Get dt + try: + dt_factor = getattr(self.backend.configuration(), "dt") + except AttributeError as no_dt: + raise AttributeError("dt parameter is missing in backend configuration") from no_dt + + # Get center frequency from backend + if self._absolute: + center_freq = self.backend.defaults().qubit_freq_est[self.physical_qubits[0]] + else: + center_freq = None + # Create the circuits to run circs = [] for freq in self._frequencies: - freq_shift = freq if self._absolute: - center_freq = backend.defaults().qubit_freq_est[self.physical_qubits[0]] freq_shift -= center_freq - freq_shift = np.round(freq_shift, decimals=3) assigned_circ = circuit.assign_parameters({freq_param: freq_shift}, inplace=False) @@ -213,13 +222,9 @@ def circuits(self, backend: Optional[Backend] = None): "sigma": self.experiment_options.sigma, "width": self.experiment_options.width, "schedule": str(sched), + "dt": dt_factor, } - try: - assigned_circ.metadata["dt"] = getattr(backend.configuration(), "dt") - except AttributeError as no_dt: - raise QiskitError("Dt parameter is missing in backend configuration") from no_dt - circs.append(assigned_circ) return circs diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index d03b9761c1..1a01cc722e 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -16,8 +16,9 @@ from typing import List, Optional, Union import numpy as np -from qiskit.providers import Backend from qiskit.circuit import QuantumCircuit +from qiskit.providers.backend import Backend +from qiskit.test.mock import FakeBackend from qiskit_experiments.framework import BaseExperiment, Options from qiskit_experiments.library.characterization.t1_analysis import T1Analysis @@ -66,6 +67,7 @@ def __init__( self, qubit: int, delays: Union[List[float], np.array], + backend: Optional[Backend] = None, unit: Optional[str] = "s", ): """ @@ -74,6 +76,7 @@ def __init__( Args: qubit: the qubit whose T1 is to be estimated delays: delay times of the experiments + backend: Optional, the backend to run the experiment on. unit: Optional, unit of the delay times. Supported units: 's', 'ms', 'us', 'ns', 'ps', 'dt'. @@ -84,18 +87,28 @@ def __init__( raise ValueError("T1 experiment: number of delays must be at least 3") # Initialize base experiment - super().__init__([qubit]) + super().__init__([qubit], backend=backend) # Set experiment options self.set_experiment_options(delays=delays, unit=unit) - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def _set_backend(self, backend: Backend): + super()._set_backend(backend) + + # Scheduling parameters + if not self._backend.configuration().simulator and not isinstance(backend, FakeBackend): + timing_constraints = getattr(self.transpile_options, "timing_constraints", {}) + if "acquire_alignment" not in timing_constraints: + timing_constraints["aquire_aligment"] = 16 + scheduling_method = getattr(self.transpile_options, "scheduling_method", "alap") + self.set_transpile_options( + timing_constraints=timing_constraints, scheduling_method=scheduling_method + ) + + def circuits(self) -> List[QuantumCircuit]: """ Return a list of experiment circuits - Args: - backend: Optional, a backend object - Returns: The experiment circuits @@ -104,7 +117,7 @@ def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: """ if self.experiment_options.unit == "dt": try: - dt_factor = getattr(backend.configuration(), "dt") + dt_factor = getattr(self.backend.configuration(), "dt") except AttributeError as no_dt: raise AttributeError("Dt parameter is missing in backend configuration") from no_dt diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index abb93d486f..dbef1e8186 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -14,13 +14,15 @@ """ -from typing import List, Optional, Union +from typing import List, Union, Optional import numpy as np import qiskit from qiskit.utils import apply_prefix -from qiskit.providers import Backend from qiskit.circuit import QuantumCircuit +from qiskit.providers.backend import Backend +from qiskit.test.mock import FakeBackend + from qiskit_experiments.framework import BaseExperiment, Options from .t2ramsey_analysis import T2RamseyAnalysis @@ -80,47 +82,57 @@ def __init__( self, qubit: int, delays: Union[List[float], np.array], + backend: Optional[Backend] = None, unit: str = "s", osc_freq: float = 0.0, ): """ - **T2Ramsey class** - Initialize the T2Ramsey class. Args: qubit: the qubit under test. delays: delay times of the experiments. + backend: Optional, the backend to run the experiment on. unit: Optional, time unit of `delays`. Supported units: 's', 'ms', 'us', 'ns', 'ps', 'dt'. The unit is used for both T2Ramsey and for the frequency. - osc_freq: the oscillation frequency induced by the user. \ - The frequency is given in Hz. - + osc_freq: the oscillation frequency induced by the user. + The frequency is given in Hz. """ - super().__init__([qubit]) + super().__init__([qubit], backend=backend) self.set_experiment_options(delays=delays, unit=unit, osc_freq=osc_freq) - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def _set_backend(self, backend: Backend): + super()._set_backend(backend) + + # Scheduling parameters + if not self._backend.configuration().simulator and not isinstance(backend, FakeBackend): + timing_constraints = getattr(self.transpile_options, "timing_constraints", {}) + if "acquire_alignment" not in timing_constraints: + timing_constraints["aquire_aligment"] = 16 + scheduling_method = getattr(self.transpile_options, "scheduling_method", "alap") + self.set_transpile_options( + timing_constraints=timing_constraints, scheduling_method=scheduling_method + ) + + def circuits(self) -> List[QuantumCircuit]: """Return a list of experiment circuits. Each circuit consists of a Hadamard gate, followed by a fixed delay, a phase gate (with a linear phase), and an additional Hadamard gate. - Args: - backend: Optional, a backend object - Returns: The experiment circuits Raises: - AttributeError: if unit is 'dt', but 'dt' parameter is missing in the backend configuration. + AttributeError: if unit is 'dt', but 'dt' parameter is missing in + the backend configuration. """ conversion_factor = 1 if self.experiment_options.unit == "dt": try: - dt_factor = getattr(backend._configuration, "dt") + dt_factor = getattr(self.backend._configuration, "dt") conversion_factor = dt_factor except AttributeError as no_dt: raise AttributeError("Dt parameter is missing in backend configuration") from no_dt diff --git a/qiskit_experiments/library/quantum_volume/qv_experiment.py b/qiskit_experiments/library/quantum_volume/qv_experiment.py index acfea48c74..e2c0ce076e 100644 --- a/qiskit_experiments/library/quantum_volume/qv_experiment.py +++ b/qiskit_experiments/library/quantum_volume/qv_experiment.py @@ -15,7 +15,6 @@ from typing import Union, Iterable, Optional, List from numpy.random import Generator, default_rng -from qiskit.providers.backend import Backend try: from qiskit import Aer @@ -27,6 +26,7 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import QuantumVolume as QuantumVolumeCircuit from qiskit import transpile +from qiskit.providers.backend import Backend from qiskit_experiments.framework import BaseExperiment, Options from .qv_analysis import QuantumVolumeAnalysis @@ -73,6 +73,7 @@ class QuantumVolume(BaseExperiment): def __init__( self, qubits: Union[int, Iterable[int]], + backend: Optional[Backend] = None, trials: Optional[int] = 100, seed: Optional[Union[int, Generator]] = None, simulation_backend: Optional[Backend] = None, @@ -82,6 +83,7 @@ def __init__( Args: qubits: The number of qubits or list of physical qubits for the experiment. + backend: Optional, the backend to run the experiment on. trials: The number of trials to run the quantum volume circuit. seed: Seed or generator object for random number generation. If None default_rng will be used. @@ -91,7 +93,7 @@ def __init__( (in case :class:`AerSimulator` is not installed :class:`qiskit.quantum_info.Statevector` will be used). """ - super().__init__(qubits) + super().__init__(qubits, backend=backend) # Set configurable options self.set_experiment_options(trials=trials) @@ -150,12 +152,9 @@ def _get_ideal_data(self, circuit: QuantumCircuit, **run_options) -> List[float] probabilities = state_vector.probabilities() return probabilities - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Return a list of Quantum Volume circuits. - Args: - backend (Backend): Optional, a backend object. - Returns: A list of :class:`QuantumCircuit`. """ diff --git a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py index 5b593254da..b6c9ce92a5 100644 --- a/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/interleaved_rb_experiment.py @@ -20,6 +20,7 @@ from qiskit.circuit import Instruction from qiskit.quantum_info import Clifford from qiskit.exceptions import QiskitError +from qiskit.providers.backend import Backend from .rb_experiment import StandardRB from .interleaved_rb_analysis import InterleavedRBAnalysis @@ -51,6 +52,7 @@ def __init__( interleaved_element: Union[QuantumCircuit, Instruction, Clifford], qubits: Union[int, Iterable[int]], lengths: Iterable[int], + backend: Optional[Backend] = None, num_samples: int = 3, seed: Optional[Union[int, Generator]] = None, full_sampling: bool = False, @@ -63,6 +65,7 @@ def __init__( qubits: The number of qubits or list of physical qubits for the experiment. lengths: A list of RB sequences lengths. + backend: The backend to run the experiment on. num_samples: Number of samples to generate for each sequence length seed: Seed or generator object for random number @@ -73,7 +76,14 @@ def __init__( Clifford samples to shorter sequences. """ self._set_interleaved_element(interleaved_element) - super().__init__(qubits, lengths, num_samples, seed, full_sampling) + super().__init__( + qubits, + lengths, + backend=backend, + num_samples=num_samples, + seed=seed, + full_sampling=full_sampling, + ) def _sample_circuits(self, lengths, seed=None): circuits = [] diff --git a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py index f0c33c2db8..231b7eee06 100644 --- a/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py +++ b/qiskit_experiments/library/randomized_benchmarking/rb_experiment.py @@ -18,9 +18,9 @@ from numpy.random import Generator, default_rng from qiskit import QuantumCircuit, QiskitError -from qiskit.providers import Backend from qiskit.quantum_info import Clifford from qiskit.circuit import Gate +from qiskit.providers.backend import Backend import qiskit_experiments.data_processing as dp from qiskit_experiments.framework import BaseExperiment, ParallelExperiment, Options @@ -63,6 +63,7 @@ def __init__( self, qubits: Union[int, Iterable[int]], lengths: Iterable[int], + backend: Optional[Backend] = None, num_samples: int = 3, seed: Optional[Union[int, Generator]] = None, full_sampling: Optional[bool] = False, @@ -73,6 +74,7 @@ def __init__( qubits: The number of qubits or list of physical qubits for the experiment. lengths: A list of RB sequences lengths. + backend: The backend to run the experiment on. num_samples: Number of samples to generate for each sequence length. seed: Seed or generator object for random number generation. If None default_rng will be used. @@ -83,7 +85,7 @@ def __init__( The default is False. """ # Initialize base experiment - super().__init__(qubits) + super().__init__(qubits, backend=backend) self._verify_parameters(lengths, num_samples) # Set configurable options @@ -133,13 +135,9 @@ def _default_experiment_options(cls) -> Options: return options - # pylint: disable = arguments-differ - def circuits(self, backend: Optional[Backend] = None) -> List[QuantumCircuit]: + def circuits(self) -> List[QuantumCircuit]: """Return a list of RB circuits. - Args: - backend (Backend): Optional, a backend object. - Returns: A list of :class:`QuantumCircuit`. """ @@ -231,7 +229,7 @@ def _get_circuit_metadata(self, circuit): return meta return None - def _postprocess_transpiled_circuits(self, circuits, backend, **run_options): + def _postprocess_transpiled_circuits(self, circuits, **run_options): """Additional post-processing of transpiled circuits before running on backend""" for c in circuits: meta = self._get_circuit_metadata(c) diff --git a/qiskit_experiments/library/tomography/tomography_experiment.py b/qiskit_experiments/library/tomography/tomography_experiment.py index ee79475f85..3685c3e2b1 100644 --- a/qiskit_experiments/library/tomography/tomography_experiment.py +++ b/qiskit_experiments/library/tomography/tomography_experiment.py @@ -19,6 +19,7 @@ import numpy as np from qiskit.circuit import QuantumCircuit, Instruction from qiskit.circuit.library import Permutation +from qiskit.providers.backend import Backend from qiskit.quantum_info.operators.base_operator import BaseOperator import qiskit.quantum_info as qi from qiskit_experiments.exceptions import QiskitError @@ -53,6 +54,7 @@ def _default_experiment_options(cls) -> Options: def __init__( self, circuit: Union[QuantumCircuit, Instruction, BaseOperator], + backend: Optional[Backend] = None, measurement_basis: Optional[BaseTomographyMeasurementBasis] = None, measurement_qubits: Optional[Iterable[int]] = None, preparation_basis: Optional[BaseTomographyPreparationBasis] = None, @@ -65,6 +67,7 @@ def __init__( Args: circuit: the quantum process circuit. If not a quantum circuit it must be a class that can be appended to a quantum circuit. + backend: The backend to run the experiment on. measurement_basis: Tomography basis for measurements. measurement_qubits: Optional, the qubits to be measured. These should refer to the logical qubits in the state circuit. @@ -81,7 +84,7 @@ def __init__( # Initialize BaseExperiment if qubits is None: qubits = circuit.num_qubits - super().__init__(qubits) + super().__init__(qubits, backend=backend) # Get the target tomography circuit if isinstance(circuit, QuantumCircuit): @@ -150,7 +153,7 @@ def _metadata(self): metadata["target"] = copy.copy(self._target) return metadata - def circuits(self, backend=None): + def circuits(self): # Get qubits and clbits meas_qubits = self._meas_qubits or range(self.num_qubits) diff --git a/releasenotes/notes/exp-backend-691201fd10046566.yaml b/releasenotes/notes/exp-backend-691201fd10046566.yaml new file mode 100644 index 0000000000..f941d03737 --- /dev/null +++ b/releasenotes/notes/exp-backend-691201fd10046566.yaml @@ -0,0 +1,22 @@ +--- +features: + - | + Add ``backend`` as an optional ``__init__`` kwarg for all experiments to + alow setting the backend at initialization. The backand can also be set + and retrieved from the experiment object after construction using the + :meth:`~qiskit_experiments.framework.BaseExperiment.backend` + property and setter. + + When using the ``backend`` kwarg of + :meth:`~qiskit_experiments.framework.BaseExperiment.run` to specify + a backend this will temporarily override any currently set backends + for that single execution. +developer: + - | + Added a ``_set_backend`` method to + :class:`~qiskit_experiments.framework.BaseExperiment` that is called + when a backend is set via initalization or the ``backend`` setter. This + can be overridden in experiment subclasses if required. For example this + could be used to extract any needed configuration or properties from the + specified backend, or to update experiment options of configuration based + on the backend. diff --git a/test/calibration/experiments/test_drag.py b/test/calibration/experiments/test_drag.py index 4e182f9f8a..48d79d9126 100644 --- a/test/calibration/experiments/test_drag.py +++ b/test/calibration/experiments/test_drag.py @@ -109,7 +109,8 @@ def test_default_circuits(self): drag = DragCal(0) drag.set_experiment_options(reps=[2, 4, 8], schedule=self.x_plus) - circuits = drag.circuits(DragBackend(gate_name="xp")) + drag.backend = DragBackend(gate_name="xp") + circuits = drag.circuits() for idx, expected in enumerate([4, 8, 16]): ops = transpile(circuits[idx * 51], backend).count_ops() diff --git a/test/calibration/experiments/test_fine_drag.py b/test/calibration/experiments/test_fine_drag.py index 5eb7aa0ae6..f53f5f7f59 100644 --- a/test/calibration/experiments/test_fine_drag.py +++ b/test/calibration/experiments/test_fine_drag.py @@ -50,8 +50,8 @@ def test_circuits(self): drag = FineDrag(0) drag.set_experiment_options(schedule=self.schedule) - - for circuit in drag.circuits(FakeArmonk())[1:]: + drag.backend = FakeArmonk() + for circuit in drag.circuits()[1:]: for idx, name in enumerate(["Drag", "rz", "Drag", "rz"]): self.assertEqual(circuit.data[idx][0].name, name) diff --git a/test/calibration/experiments/test_rabi.py b/test/calibration/experiments/test_rabi.py index 0e1d6ad51e..da7bca0be0 100644 --- a/test/calibration/experiments/test_rabi.py +++ b/test/calibration/experiments/test_rabi.py @@ -141,7 +141,8 @@ def test_ef_rabi_circuit(self): anharm = -330e6 rabi12 = EFRabi(2) rabi12.set_experiment_options(amplitudes=[0.5], frequency_shift=anharm) - circ = rabi12.circuits(RabiBackend())[0] + rabi12.backend = RabiBackend() + circ = rabi12.circuits()[0] with pulse.build() as expected: pulse.shift_frequency(anharm, pulse.DriveChannel(2)) @@ -161,7 +162,8 @@ def test_default_schedule(self): rabi = Rabi(2) rabi.set_experiment_options(amplitudes=[0.5]) - circs = rabi.circuits(RabiBackend()) + rabi.backend = RabiBackend() + circs = rabi.circuits() with pulse.build() as expected: pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(2)) @@ -179,7 +181,8 @@ def test_user_schedule(self): rabi = Rabi(2) rabi.set_experiment_options(schedule=my_schedule, amplitudes=[0.5]) - circs = rabi.circuits(RabiBackend()) + rabi.backend = RabiBackend() + circs = rabi.circuits() assigned_sched = my_schedule.assign_parameters({amp: 0.5}, inplace=False) self.assertEqual(circs[0].calibrations["Rabi"][((2,), (0.5,))], assigned_sched) diff --git a/test/calibration/experiments/test_rough_frequency.py b/test/calibration/experiments/test_rough_frequency.py index 69a013bb81..1dc2d220ed 100644 --- a/test/calibration/experiments/test_rough_frequency.py +++ b/test/calibration/experiments/test_rough_frequency.py @@ -37,7 +37,9 @@ def test_init(self): auto_update = False absolute = False - freq = RoughFrequencyCal(qubit, cals, frequencies, unit, auto_update, absolute) + freq = RoughFrequencyCal( + qubit, cals, frequencies, unit=unit, auto_update=auto_update, absolute=absolute + ) self.assertEqual(freq.physical_qubits, (qubit,)) self.assertEqual(freq._frequencies, [1000, 2000, 3000]) diff --git a/test/fake_experiment.py b/test/fake_experiment.py index be11cbb244..f5abddba5a 100644 --- a/test/fake_experiment.py +++ b/test/fake_experiment.py @@ -37,6 +37,6 @@ def __init__(self, qubits=1): """Initialise the fake experiment.""" super().__init__(qubits) - def circuits(self, backend=None): + def circuits(self): """Fake circuits.""" return [] diff --git a/test/test_cross_resonance_hamiltonian.py b/test/test_cross_resonance_hamiltonian.py index a384d06543..c93d52bf25 100644 --- a/test/test_cross_resonance_hamiltonian.py +++ b/test/test_cross_resonance_hamiltonian.py @@ -160,8 +160,6 @@ class TestCrossResonanceHamiltonian(QiskitTestCase): def test_circuit_generation(self): """Test generated circuits.""" - backend = CrossResonanceHamiltonianBackend() - expr = cr_hamiltonian.CrossResonanceHamiltonian( qubits=(0, 1), flat_top_widths=[1000], @@ -170,6 +168,7 @@ def test_circuit_generation(self): sigma=64, risefall=2, ) + expr.backend = CrossResonanceHamiltonianBackend() nearlest_16 = 1248 @@ -187,7 +186,7 @@ def test_circuit_generation(self): pulse.delay(nearlest_16, pulse.DriveChannel(1)) cr_gate = circuit.Gate("cr_gate", num_qubits=2, params=[1000]) - expr_circs = expr.circuits(backend) + expr_circs = expr.circuits() x0_circ = QuantumCircuit(2, 1) x0_circ.append(cr_gate, [0, 1]) @@ -231,8 +230,6 @@ def test_circuit_generation(self): def test_circuit_generation_from_sec(self): """Test generated circuits when time unit is sec.""" - backend = CrossResonanceHamiltonianBackend() - expr = cr_hamiltonian.CrossResonanceHamiltonian( qubits=(0, 1), flat_top_widths=[500], @@ -241,6 +238,7 @@ def test_circuit_generation_from_sec(self): sigma=20, risefall=2, ) + expr.backend = CrossResonanceHamiltonianBackend() nearlest_16 = 576 @@ -258,7 +256,7 @@ def test_circuit_generation_from_sec(self): pulse.delay(nearlest_16, pulse.DriveChannel(1)) cr_gate = circuit.Gate("cr_gate", num_qubits=2, params=[500]) - expr_circs = expr.circuits(backend) + expr_circs = expr.circuits() x0_circ = QuantumCircuit(2, 1) x0_circ.append(cr_gate, [0, 1]) diff --git a/test/test_framework.py b/test/test_framework.py index 99c220b7f1..389b3a4eb6 100644 --- a/test/test_framework.py +++ b/test/test_framework.py @@ -35,7 +35,7 @@ def test_job_splitting(self, max_experiments): class Experiment(FakeExperiment): """Fake Experiment to test job splitting""" - def circuits(self, backend=None): + def circuits(self): """Generate fake circuits""" qc = QuantumCircuit(1) qc.measure_all() diff --git a/test/test_qubit_spectroscopy.py b/test/test_qubit_spectroscopy.py index 81ef75cb8b..ecf63b015c 100644 --- a/test/test_qubit_spectroscopy.py +++ b/test/test_qubit_spectroscopy.py @@ -134,6 +134,7 @@ def test_spectroscopy12_end2end_classified(self): # Note that the backend is not sophisticated enough to simulate an e-f # transition so we run the test with g-e. spec = EFSpectroscopy(qubit, frequencies, unit="Hz") + spec.backend = backend spec.set_run_options(meas_level=MeasLevel.CLASSIFIED) expdata = spec.run(backend) expdata.block_for_results() @@ -144,6 +145,6 @@ def test_spectroscopy12_end2end_classified(self): self.assertEqual(result.quality, "good") # Test the circuits - circ = spec.circuits(backend)[0] + circ = spec.circuits()[0] self.assertEqual(circ.data[0][0].name, "x") self.assertEqual(circ.data[1][0].name, "Spec")