Source code for qiskit_experiments.framework.base_experiment
# This code is part of Qiskit.## (C) Copyright IBM 2021.## 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."""Base Experiment class."""fromabcimportABC,abstractmethodimportcopyfromcollectionsimportOrderedDictfromtypingimportSequence,Optional,Tuple,List,Dict,Unionfromqiskitimporttranspile,QuantumCircuitfromqiskit.providersimportJob,Backendfromqiskit.exceptionsimportQiskitErrorfromqiskit.qobj.utilsimportMeasLevelfromqiskit.providers.optionsimportOptionsfromqiskit_experiments.frameworkimportBackendDatafromqiskit_experiments.framework.store_init_argsimportStoreInitArgsfromqiskit_experiments.framework.base_analysisimportBaseAnalysisfromqiskit_experiments.framework.experiment_dataimportExperimentDatafromqiskit_experiments.framework.configsimportExperimentConfigfromqiskit_experiments.warningsimportdeprecate_arguments
[docs]classBaseExperiment(ABC,StoreInitArgs):"""Abstract base class for experiments."""@deprecate_arguments({"qubits":"physical_qubits"},"0.5")def__init__(self,physical_qubits:Sequence[int],analysis:Optional[BaseAnalysis]=None,backend:Optional[Backend]=None,experiment_type:Optional[str]=None,):"""Initialize the experiment object. Args: physical_qubits: list of physical qubits for the experiment. analysis: Optional, the analysis to use for the experiment. backend: Optional, the backend to run the experiment on. experiment_type: Optional, the experiment type string. Raises: QiskitError: If qubits contains duplicates. """# Experiment identification metadataself._type=experiment_typeifexperiment_typeelsetype(self).__name__# Circuit parametersself._num_qubits=len(physical_qubits)self._physical_qubits=tuple(physical_qubits)ifself._num_qubits!=len(set(self._physical_qubits)):raiseQiskitError("Duplicate qubits in physical qubits list.")# Experiment optionsself._experiment_options=self._default_experiment_options()self._transpile_options=self._default_transpile_options()self._run_options=self._default_run_options()# Store keys of non-default optionsself._set_experiment_options=set()self._set_transpile_options=set()self._set_run_options=set()self._set_analysis_options=set()# Set analysisself._analysis=Noneifanalysis:self.analysis=analysis# Set backend# This should be called last in case `_set_backend` access any of the# attributes created during initializationself._backend=Noneself._backend_data=Noneifisinstance(backend,Backend):self._set_backend(backend)@propertydefexperiment_type(self)->str:"""Return experiment type."""returnself._type@propertydefphysical_qubits(self)->Tuple[int,...]:"""Return the device qubits for the experiment."""returnself._physical_qubits@propertydefnum_qubits(self)->int:"""Return the number of qubits for the experiment."""returnself._num_qubits@propertydefanalysis(self)->Union[BaseAnalysis,None]:"""Return the analysis instance for the experiment"""returnself._analysis@analysis.setterdefanalysis(self,analysis:Union[BaseAnalysis,None])->None:"""Set the analysis instance for the experiment"""ifanalysisisnotNoneandnotisinstance(analysis,BaseAnalysis):raiseTypeError("Input is not a None or a BaseAnalysis subclass.")self._analysis=analysis@propertydefbackend(self)->Union[Backend,None]:"""Return the backend for the experiment"""returnself._backend@backend.setterdefbackend(self,backend:Union[Backend,None])->None:"""Set the backend for the experiment"""ifnotisinstance(backend,Backend):raiseTypeError("Input is not a backend.")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=backendself._backend_data=BackendData(backend)
[docs]defcopy(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)ifself.analysis:ret.analysis=self.analysis.copy()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._set_experiment_options=copy.copy(self._set_experiment_options)ret._set_transpile_options=copy.copy(self._set_transpile_options)ret._set_run_options=copy.copy(self._set_run_options)returnret
[docs]defconfig(self)->ExperimentConfig:"""Return the config dataclass for this experiment"""args=tuple(getattr(self,"__init_args__",OrderedDict()).values())kwargs=dict(getattr(self,"__init_kwargs__",OrderedDict()))# Only store non-default valued optionsexperiment_options=dict((key,getattr(self._experiment_options,key))forkeyinself._set_experiment_options)transpile_options=dict((key,getattr(self._transpile_options,key))forkeyinself._set_transpile_options)run_options=dict((key,getattr(self._run_options,key))forkeyinself._set_run_options)returnExperimentConfig(cls=type(self),args=args,kwargs=kwargs,experiment_options=experiment_options,transpile_options=transpile_options,run_options=run_options,)
[docs]@classmethoddeffrom_config(cls,config:Union[ExperimentConfig,Dict])->"BaseExperiment":"""Initialize an experiment from experiment config"""ifisinstance(config,dict):config=ExperimentConfig(**dict)ret=cls(*config.args,**config.kwargs)ifconfig.experiment_options:ret.set_experiment_options(**config.experiment_options)ifconfig.transpile_options:ret.set_transpile_options(**config.transpile_options)ifconfig.run_options:ret.set_run_options(**config.run_options)returnret
[docs]defrun(self,backend:Optional[Backend]=None,analysis:Optional[Union[BaseAnalysis,None]]="default",timeout:Optional[float]=None,**run_options,)->ExperimentData:"""Run an experiment and perform analysis. Args: backend: Optional, the backend to run the experiment on. This will override any currently set backends for the single execution. analysis: Optional, a custom analysis instance to use for performing analysis. If None analysis will not be run. If ``"default"`` the experiments :meth:`analysis` instance will be used if it contains one. timeout: Time to wait for experiment jobs to finish running before cancelling. run_options: backend runtime options used for circuit execution. Returns: The experiment data object. Raises: QiskitError: If experiment is run with an incompatible existing ExperimentData container. """ifbackendisnotNoneoranalysis!="default"orrun_options:# Make a copy to update analysis or backend if one is provided at runtimeexperiment=self.copy()ifbackend:experiment._set_backend(backend)ifisinstance(analysis,BaseAnalysis):experiment.analysis=analysisifrun_options:experiment.set_run_options(**run_options)else:experiment=selfifexperiment.backendisNone:raiseQiskitError("Cannot run experiment, no backend has been set.")# Finalize experiment before executionsexperiment._finalize()# Generate and transpile circuitstranspiled_circuits=experiment._transpiled_circuits()# Initialize result containerexperiment_data=experiment._initialize_experiment_data()# Run optionsrun_opts=experiment.run_options.__dict__# Run jobsjobs=experiment._run_jobs(transpiled_circuits,**run_opts)experiment_data.add_jobs(jobs,timeout=timeout)# Optionally run analysisifanalysisandexperiment.analysis:returnexperiment.analysis.run(experiment_data)else:returnexperiment_data
def_initialize_experiment_data(self)->ExperimentData:"""Initialize the return data container for the experiment run"""returnExperimentData(experiment=self)def_finalize(self):"""Finalize experiment object before running jobs. Subclasses can override this method to set any final option values derived from other options or attributes of the experiment before `_run` is called. """passdef_run_jobs(self,circuits:List[QuantumCircuit],**run_options)->List[Job]:"""Run circuits on backend as 1 or more jobs."""# Get max circuits for job splittingmax_circuits_option=getattr(self.experiment_options,"max_circuits",None)max_circuits_backend=self._backend_data.max_circuitsifmax_circuits_optionandmax_circuits_backend:max_circuits=min(max_circuits_option,max_circuits_backend)elifmax_circuits_option:max_circuits=max_circuits_optionelse:max_circuits=max_circuits_backend# Run experiment jobsifmax_circuitsandlen(circuits)>max_circuits:# Split jobs for backends that have a maximum job sizejob_circuits=[circuits[i:i+max_circuits]foriinrange(0,len(circuits),max_circuits)]else:# Run as single jobjob_circuits=[circuits]# Run jobsjobs=[self.backend.run(circs,**run_options)forcircsinjob_circuits]returnjobs
[docs]@abstractmethoddefcircuits(self)->List[QuantumCircuit]:"""Return a list of experiment circuits. Returns: A list of :class:`~qiskit.circuit.QuantumCircuit`. .. note:: These circuits should be on qubits ``[0, .., N-1]`` for an *N*-qubit experiment. The circuits mapped to physical qubits are obtained via the internal :meth:`_transpiled_circuits` method. """
# NOTE: Subclasses should override this method using the `options`# values for any explicit experiment options that affect circuit# generationdef_transpiled_circuits(self)->List[QuantumCircuit]:"""Return a list of experiment circuits, transpiled. This function can be overridden to define custom transpilation. """transpile_opts=copy.copy(self.transpile_options.__dict__)transpile_opts["initial_layout"]=list(self.physical_qubits)transpiled=transpile(self.circuits(),self.backend,**transpile_opts)returntranspiled@classmethoddef_default_experiment_options(cls)->Options:"""Default experiment options. Experiment Options: max_circuits (Optional[int]): The maximum number of circuits per job when running an experiment on a backend. """# Experiment subclasses should override this method to return# an `Options` object containing all the supported options for# that experiment and their default values. Only options listed# here can be modified later by the different methods for# setting options.returnOptions(max_circuits=None)@propertydefexperiment_options(self)->Options:"""Return the options for the experiment."""returnself._experiment_options
[docs]defset_experiment_options(self,**fields):"""Set the experiment options. Args: fields: The fields to update the options Raises: AttributeError: If the field passed in is not a supported options """forfieldinfields:ifnothasattr(self._experiment_options,field):raiseAttributeError(f"Options field {field} is not valid for {type(self).__name__}")self._experiment_options.update_options(**fields)self._set_experiment_options=self._set_experiment_options.union(fields)
@classmethoddef_default_transpile_options(cls)->Options:"""Default transpiler options for transpilation of circuits"""# Experiment subclasses can override this method if they need# to set specific default transpiler options to transpile the# experiment circuits.returnOptions(optimization_level=0)@propertydeftranspile_options(self)->Options:"""Return the transpiler options for the :meth:`run` method."""returnself._transpile_options
[docs]defset_transpile_options(self,**fields):"""Set the transpiler options for :meth:`run` method. Args: fields: The fields to update the options Raises: QiskitError: If `initial_layout` is one of the fields. .. seealso:: The :ref:`guide_setting_options` guide for code example. """if"initial_layout"infields:raiseQiskitError("Initial layout cannot be specified as a transpile option"" as it is determined by the experiment physical qubits.")self._transpile_options.update_options(**fields)self._set_transpile_options=self._set_transpile_options.union(fields)
@classmethoddef_default_run_options(cls)->Options:"""Default options values for the experiment :meth:`run` method."""returnOptions(meas_level=MeasLevel.CLASSIFIED)@propertydefrun_options(self)->Options:"""Return options values for the experiment :meth:`run` method."""returnself._run_options
[docs]defset_run_options(self,**fields):"""Set options values for the experiment :meth:`run` method. Args: fields: The fields to update the options .. seealso:: The :ref:`guide_setting_options` guide for code example. """self._run_options.update_options(**fields)self._set_run_options=self._set_run_options.union(fields)
def_metadata(self)->Dict[str,any]:"""Return experiment metadata for ExperimentData. Subclasses can override this method to add custom experiment metadata to the returned experiment result data. """metadata={"physical_qubits":list(self.physical_qubits)}returnmetadatadef__json_encode__(self):"""Convert to format that can be JSON serialized"""returnself.config()@classmethoddef__json_decode__(cls,value):"""Load from JSON compatible format"""returncls.from_config(value)