Source code for qiskit_experiments.library.randomized_benchmarking.layer_fidelity_unitary
# This code is part of Qiskit.## (C) Copyright IBM 2023.## 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."""Layer Fidelity Unitary RB Experiment class."""importfunctoolsimportloggingimportwarningsfromtypingimportUnion,Iterable,Optional,List,Sequence,Tuple,Dictimportnumpyasnpfromnumpy.randomimportGenerator,default_rngfromnumpy.random.bit_generatorimportBitGenerator,SeedSequencefromqiskit.circuitimportQuantumCircuit,Instruction,CircuitInstruction,Barrier,Gatefromqiskit.circuit.libraryimportget_standard_gate_name_mappingfromqiskit.transpilerimportCouplingMap,generate_preset_pass_manager,Targetfromqiskit.exceptionsimportQiskitErrorfromqiskit.providers.backendimportBackendfromqiskit.quantum_infoimportOperatorfromqiskit_experiments.frameworkimportBaseExperiment,Optionsfromqiskit_experiments.framework.configsimportExperimentConfigfrom.clifford_utilsimport(CliffordUtils,DEFAULT_SYNTHESIS_METHOD,_clifford_1q_int_to_instruction,_decompose_clifford_ops,)from.layer_fidelity_analysisimportLayerFidelityAnalysisLOG=logging.getLogger(__name__)GATE_NAME_MAP=get_standard_gate_name_mapping()NUM_1Q_CLIFFORD=CliffordUtils.NUM_CLIFFORD_1_QUBIT
[docs]classLayerFidelityUnitary(BaseExperiment):r"""A holistic benchmarking experiment to characterize the full quality of the devices at scale. # section: overview Unitary Layer Fidelity (ULF) is a method to estimate the fidelity of a connecting set of arbitrary two-qubit gates over :math:`N` qubits by measuring gate errors using simultaneous direct unitary randomized benchmarking (RB) in disjoint layers. LF can easily be expressed as a layer size independent quantity, error per layered gate (EPLG): :math:`EPLG = 1 - LF^{1/N_{2Q}}` where :math:`N_{2Q}` is number of 2-qubit gates in the layers. Each of the 2-qubit (or 1-qubit) direct RBs yields the decaying probabilities to get back to the ground state for an increasing sequence length (i.e. number of layers), fits the exponential curve to estimate the decay rate, and calculates the process fidelity of the subsystem from the rate. LF is calculated as the product of the 2-qubit (or 1-qubit) process fidelities. See Ref. [1] for details. This unitary version allows artibrary 2Q gates # section: analysis_ref :class:`LayerFidelityAnalysis` # section: reference .. ref_arxiv:: 1 2311.05933 # section: example .. jupyter-execute:: :hide-code: # backend from qiskit_aer import AerSimulator from qiskit_aer.noise import NoiseModel, depolarizing_error noise_model = NoiseModel() noise_model.add_all_qubit_quantum_error(depolarizing_error(5e-3, 1), ["sx", "x"]) noise_model.add_all_qubit_quantum_error(depolarizing_error(0, 1), ["rz"]) noise_model.add_all_qubit_quantum_error(depolarizing_error(5e-2, 2), ["rzz"]) backend = AerSimulator(noise_model=noise_model) .. jupyter-execute:: import numpy as np from qiskit import QuantumCircuit from qiskit.circuit.library import RZZGate from qiskit_experiments.library.randomized_benchmarking import LayerFidelityUnitary lengths = np.arange(1, 80, 10) two_qubit_layers=[[(0, 1), (3, 5)], [(1, 3), (5, 6)]] num_samples = 3 seed = 106 # Can load this way if benchmarking a generic circuit # qc = QuantumCircuit(2) # qc.rzz(0.5,0,1) # two_qubit_gates=[qc.to_instruction()] exp = LayerFidelityUnitary( physical_qubits=[0, 1, 3, 5, 6], two_qubit_layers=two_qubit_layers, lengths=lengths, backend=backend, num_samples=num_samples, seed=seed, two_qubit_gates=[RZZGate(0.5)], ) exp_data = exp.run().block_for_results() results = exp_data.analysis_results(dataframe=True) display(exp_data.figure(0)) # one of 6 figures display(exp_data.analysis_results("EPLG", dataframe=True)) print(f"Available results: {set(results.name)}") """def__init__(self,physical_qubits:Sequence[int],two_qubit_layers:Sequence[Sequence[Tuple[int,int]]],lengths:Iterable[int],two_qubit_gates:Sequence[Union[Instruction,Gate]],num_samples:int=6,backend:Optional[Backend]=None,seed:Optional[Union[int,SeedSequence,BitGenerator,Generator]]=None,two_qubit_basis_gates:Optional[Sequence[str]]=None,one_qubit_basis_gates:Optional[Sequence[str]]=None,layer_barrier:Optional[bool]=True,min_delay:Optional[Sequence[int]]=None,):"""Initialize a unitary layer fidelity experiment. Args: physical_qubits: List of physical qubits for the experiment. two_qubit_layers: List of two-qubit gate layers to run on. Each two-qubit gate layer must be given as a list of directed qubit pairs. lengths: A list of layer lengths (the number of depth points). two_qubit_gates: A list of two qubit circuit instructions or gates that will be in the entangling layer. If more than one than they are sampled from this list. These are assumed to be the backend ISA already. num_samples: Number of samples (i.e. circuits) to generate for each layer length. backend: Optional, the backend to run the experiment on. Note that either ``backend`` or ``two_qubit_gate`` and ``one_qubit_basis_gates`` must be set at instantiation. seed: Optional, seed used to initialize ``numpy.random.default_rng``. when generating circuits. The ``default_rng`` will be initialized with this seed value every time :meth:~.LayerFidelity.circuits` is called. two_qubit_basis_gates: Optional, 2q-gates to use for transpiling the inverse. If not specified (but ``backend`` is supplied), all 2q-gates supported in the backend are automatically set. one_qubit_basis_gates: Optional, 1q-gates to use for implementing 1q-Clifford operations. If not specified (but ``backend`` is supplied), all 1q-gates supported in the backend are automatically set. layer_barrier (bool): Optional, enforce a barrier across the whole layer. Default is True, which is the defined protocol for layer fidelity. If this is set to false the code runs simultaneous direct 1+2Q RB without a barrier across all qubits. min_delay: Optional. Define a minimum delay in each 2Q layer in units of dt. This delay operation will be applied in any 1Q edge of the layer during the 2Q gate layer in order to enforce a minimum duration of the 2Q layer. This enables some crosstalk testing by removing a gate from the layer without changing the layer duration. If not None then is a list equal in length to the number of two_qubit_layers. Note that this options requires at least one 1Q edge (a qubit in physical_qubits but not in two_qubit_layers) to be applied. Also will not have an impact on the 2Q gates if layer_barrier=False. Raises: QiskitError: If any invalid argument is supplied. """# Compute full layersfull_layers=[]fortwo_q_layerintwo_qubit_layers:qubits_in_layer={qforqpairintwo_q_layerforqinqpair}iflen(qubits_in_layer)!=2*len(two_q_layer):raiseQiskitError("two_qubit_layers have a layer with gates on non-disjoint qubits")forqinqubits_in_layer:ifqnotinphysical_qubits:raiseQiskitError(f"Qubit {q} in two_qubit_layers is not in physical_qubits")layer=two_q_layer+[(q,)forqinphysical_qubitsifqnotinqubits_in_layer]full_layers.append(layer)# Initialize base experimentsuper().__init__(physical_qubits,analysis=LayerFidelityAnalysis(full_layers),backend=backend)# assert isinstance(backend, BackendV2)# Verify parametersiflen(set(lengths))!=len(lengths):raiseQiskitError(f"The lengths list {lengths} should not contain duplicate elements.")ifnum_samples<=0:raiseQiskitError(f"The number of samples {num_samples} should be positive.")iftwo_qubit_gatesisNone:raiseQiskitError("Must specify a set of two qubit gate circuits.")iftwo_qubit_basis_gatesisNone:ifself.backendisNone:raiseQiskitError("two_qubit_basis_gates or backend must be supplied.")# Try to set default two qubit basis gate from backendtwo_qubit_basis_gates=[]foropinself.backend.target.operations:ifisinstance(op,Gate)andop.num_qubits==2:two_qubit_basis_gates.append(op.name)LOG.info("%s is set for two_qubit_gate",op.name)breakifnottwo_qubit_basis_gates:raiseQiskitError("two_qubit_gate is not provided and failed to set from backend.")ifone_qubit_basis_gatesisNone:ifself.backendisNone:raiseQiskitError("one_qubit_basis_gates or backend must be supplied.")# Try to set default one_qubit_basis_gates from backendone_qubit_basis_gates=[]foropinself.backend.target.operations:ifisinstance(op,Gate)andop.num_qubits==1:ifop.nameinGATE_NAME_MAP:one_qubit_basis_gates.append(op.name)else:warnings.warn(f'Not using single qubit gate "{op.name}". Please '"open an issue if support for using gates outside ""of Qiskit's standard gates is needed for layer ""fidelity.")LOG.info("%s is set for one_qubit_basis_gates",str(one_qubit_basis_gates))ifnotone_qubit_basis_gates:raiseQiskitError("one_qubit_basis_gates is not provided and failed to set from backend.")else:ifself.backendisNone:forgateinone_qubit_basis_gates:ifgatenotinGATE_NAME_MAP:raiseQiskitError(f"Unknown gate in one_qubit_basis_gates: {gate}.")# Set configurable optionsself.set_experiment_options(lengths=sorted(lengths),num_samples=num_samples,seed=seed,two_qubit_layers=two_qubit_layers,two_qubit_gates=two_qubit_gates,two_qubit_basis_gates=tuple(two_qubit_basis_gates),one_qubit_basis_gates=tuple(one_qubit_basis_gates),layer_barrier=layer_barrier,min_delay=min_delay,)# Verify two_qubit_gate and one_qubit_basis_gatesself.__validate_basis_gates()@classmethoddef_default_experiment_options(cls)->Options:"""Default experiment options. Experiment Options: two_qubit_layers (List[List[Tuple[int, int]]]): List of two-qubit gate layers to run on. Each two-qubit gate layer must be given as a list of directed qubit pairs. lengths (List[int]): A list of layer lengths. num_samples (int): Number of samples to generate for each layer length. seed (None or int or SeedSequence or BitGenerator or Generator): A seed used to initialize ``numpy.random.default_rng`` when generating circuits. The ``default_rng`` will be initialized with this seed value every time :meth:`circuits` is called. two_qubit_gates (list of gates or circuit instructions): Two qubit circuits two_qubit_basis_gates (Tuple[str]): Two-qubit gates to use for implementing inverse. one_qubit_basis_gates (Tuple[str]): One-qubit gates to use for implementing 1q Cliffords. clifford_synthesis_method (str): The name of the Clifford synthesis plugin to use for building circuits of RB sequences. See :ref:`synth-methods-lbl`. layer_barrier (bool): Optional, enforce a barrier across the whole layer. Default is True, which is the defined protocol for layer fidelity. If this is set to false the code runs simultaneous direct 1+2Q RB without a barrier across all qubits. min_delay (List[int]): Optional. Define a minimum delay in each 2Q layer in units of dt. This delay operation will be applied in any 1Q edge of the layer during the 2Q gate layer in order to enforce a minimum duration of the 2Q layer. This enables some crosstalk testing by removing a gate from the layer without changing the layer duration. If not None then is a list equal in length to the number of two_qubit_layers. Note that this options requires at least one 1Q edge (a qubit in physical_qubits but not in two_qubit_layers) to be applied. Also will not have an impact on the 2Q gates if layer_barrier=False. """options=super()._default_experiment_options()options.update_options(lengths=None,num_samples=None,seed=None,two_qubit_layers=None,two_qubit_gates=None,two_qubit_basis_gates=None,one_qubit_basis_gates=None,clifford_synthesis_method=DEFAULT_SYNTHESIS_METHOD,layer_barrier=True,min_delay=None,)returnoptions
[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:iffieldin{"two_qubit_layers"}:if(hasattr(self._experiment_options,field)andself._experiment_options[field]isnotNone):raiseAttributeError(f"Options field {field} is not allowed to update.")super().set_experiment_options(**fields)
@classmethoddef_default_transpile_options(cls)->Options:"""Default transpiler options for transpiling RB circuits."""returnOptions(optimization_level=1)
[docs]defset_transpile_options(self,**fields):"""Transpile options is not supported for LayerFidelity experiments. Raises: QiskitError: If `set_transpile_options` is called. """raiseQiskitError("Custom transpile options are not supported for LayerFidelity experiments.")
def_set_backend(self,backend:Backend):"""Set the backend V2 for RB experiments since RB experiments only support BackendV2."""super()._set_backend(backend)self.__validate_basis_gates()def__validate_basis_gates(self)->None:ifnotself.backend:returnopts=self.experiment_optionstarget=self.backend.target# validate two_qubit_gates listifopts.two_qubit_gates:fortwoq_gateinopts.two_qubit_gates:iftwoq_gate.num_qubits!=2:raiseQiskitError(f"{twoq_gate.name} in two_qubit_gates is not a 2Q object")ifisinstance(twoq_gate,Gate):iftwoq_gate.namenotintarget.operation_names:raiseQiskitError(f"{twoq_gate.name} in two_qubit_gates is not in backend.target")fortwo_q_layerinopts.two_qubit_layers:forqpairintwo_q_layer:ifnottarget.instruction_supported(twoq_gate.name,qpair):raiseQiskitError(f"{twoq_gate.name}{qpair} is not in backend.target")elifisinstance(twoq_gate,Instruction):forcirc_instrintwoq_gate.definition:ifnotisinstance(circ_instr,CircuitInstruction):raiseQiskitError(f"{twoq_gate.name} does not decompose into CircuitInstruction objects.")ifcirc_instr.operation.namenotintarget.operation_names:raiseQiskitError(f"{circ_instr.operation.name} in two_qubit_gates is "+"not in backend.target")ifcirc_instr.operation.num_qubits==1:forqinself.physical_qubits:ifnottarget.instruction_supported(circ_instr.operation.name,(q,)):raiseQiskitError(f"{circ_instr.operation.name}({q}) is not "+"in backend.target")ifcirc_instr.operation.num_qubits==2:fortwo_q_layerinopts.two_qubit_layers:forqpairintwo_q_layer:ifnottarget.instruction_supported(circ_instr.operation.name,qpair):raiseQiskitError(f"{circ_instr.operation.name}{qpair} is not in "+"backend.target")# validate two_qubit_basis_gates if it is setforgateinopts.two_qubit_basis_gatesor[]:ifgatenotintarget.operation_names:raiseQiskitError(f"{gate} in two_qubit_basis_gates is not in backend.target")forgateinopts.two_qubit_basis_gatesor[]:fortwo_q_layerinopts.two_qubit_layers:forqpairintwo_q_layer:ifnottarget.instruction_supported(gate,qpair):raiseQiskitError(f"{gate}{qpair} is not in backend.target")# validate one_qubit_basis_gates if it is setforgateinopts.one_qubit_basis_gatesor[]:ifgatenotintarget.operation_names:raiseQiskitError(f"{gate} in one_qubit_basis_gates is not in backend.target")forgateinopts.one_qubit_basis_gatesor[]:forqinself.physical_qubits:ifnottarget.instruction_supported(gate,(q,)):raiseQiskitError(f"{gate}({q}) is not in backend.target")def__residual_qubits(self,two_qubit_layer):qubits_in_layer={qforqpairintwo_qubit_layerforqinqpair}return[qforqinself.physical_qubitsifqnotinqubits_in_layer]
[docs]defcircuits(self)->List[QuantumCircuit]:r"""Return a list of physical circuits to measure layer fidelity. Returns: A list of :class:`QuantumCircuit`\s. """returnlist(self.circuits_generator())
[docs]defcircuits_generator(self)->Iterable[QuantumCircuit]:r"""Return a generator of physical circuits to measure layer fidelity. Returns: A generator of :class:`QuantumCircuit`\s. """opts=self.experiment_optionsresidual_qubits_by_layer=[self.__residual_qubits(layer)forlayerinopts.two_qubit_layers]rng=default_rng(seed=opts.seed)# define functions and variables for speed_to_gate_1q=functools.partial(_clifford_1q_int_to_instruction,basis_gates=opts.one_qubit_basis_gates,synthesis_method=opts.clifford_synthesis_method,)# matrices for the two qubit gatestwo_q_gate_mats=[]fortwo_q_gateinopts.two_qubit_gates:two_q_gate_mats.append(Operator(two_q_gate).to_matrix())# warn if min delay is not None and barrier is falseifopts.min_delayisnotNoneandnotopts.layer_barrier:warnings.warn("Min delay applied when layer_barrier is False.")# Circuit generationnum_qubits=max(self.physical_qubits)+1fori_sampleinrange(opts.num_samples):fori_set,(two_qubit_layer,one_qubits)inenumerate(zip(opts.two_qubit_layers,residual_qubits_by_layer)):num_2q_gates=len(two_qubit_layer)num_1q_gates=len(one_qubits)composite_qubits=two_qubit_layer+[(q,)forqinone_qubits]composite_clbits=[(2*c,2*c+1)forcinrange(num_2q_gates)]composite_clbits.extend([(c,)forcinrange(2*num_2q_gates,2*num_2q_gates+num_1q_gates)])ifopts.min_delayisNone:min_delay=Noneelse:min_delay=opts.min_delay[i_set]# cache the 1Q cliffordsoneq_cliff_mats=[CliffordUtils.clifford_1_qubit(i).to_matrix()foriinrange(24)]# generate the pass managertarget=Target.from_configuration(num_qubits=2,basis_gates=opts.two_qubit_basis_gates+opts.one_qubit_basis_gates,coupling_map=CouplingMap(((0,1),)),)pass_manager_2q=generate_preset_pass_manager(optimization_level=1,target=target,backend=self.backend,)target=Target.from_configuration(num_qubits=1,basis_gates=opts.one_qubit_basis_gates,coupling_map=None,)pass_manager_1q=generate_preset_pass_manager(optimization_level=1,target=target,backend=self.backend,)forlengthinopts.lengths:circ=QuantumCircuit(num_qubits,num_qubits)# define the barrier instructionfull_barrier_inst=CircuitInstruction(Barrier(num_qubits),circ.qubits)ifnotopts.layer_barrier:# we want separate barriers for each qubit so define them individuallybarrier_inst_gate=[]fortwo_q_gateintwo_qubit_layer:barrier_inst_gate.append(CircuitInstruction(Barrier(2),[circ.qubits[two_q_gate[0]],circ.qubits[two_q_gate[1]]],))forone_qinone_qubits:barrier_inst_gate.append(CircuitInstruction(Barrier(1),[circ.qubits[one_q]]))else:barrier_inst_gate=[full_barrier_inst]self.__circuit_body(circ,length,two_qubit_layer,one_qubits,rng,_to_gate_1q,opts.two_qubit_gates,two_q_gate_mats,barrier_inst_gate,oneq_cliff_mats,pass_manager_2q,pass_manager_1q,min_delay,)# add the measurementscirc._append(full_barrier_inst)forqubits,clbitsinzip(composite_qubits,composite_clbits):circ.measure(qubits,clbits)# store composite structure in metadatacirc.metadata={"experiment_type":"BatchExperiment","composite_metadata":[{"experiment_type":"ParallelExperiment","composite_index":list(range(len(composite_qubits))),"composite_metadata":[{"experiment_type":"SubLayerFidelity","physical_qubits":qpair,"sample":i_sample,"xval":length,}forqpairintwo_qubit_layer]+[{"experiment_type":"SubLayerFidelity","physical_qubits":(q,),"sample":i_sample,"xval":length,}forqinone_qubits],"composite_qubits":composite_qubits,"composite_clbits":composite_clbits,}],"composite_index":[i_set],}yieldcirc
@staticmethoddef__circuit_body(circ,length,two_qubit_layer,one_qubits,rng,_to_gate_1q,two_qubit_gates,two_q_gate_mats,barrier_inst_lst,oneq_cliff_mats,pass_manager_2q,pass_manager_1q,min_delay=None,):# warn if min_delay is not none and one_qubits is emptyifmin_delayisnotNoneandlen(one_qubits)==0:warnings.warn("Min delay will not be applied because there are no 1Q edges.")# start with a set of identity matricescircs_2q=[np.eye(4,dtype=complex)foriiinrange(len(two_qubit_layer))]circs_1q=[np.eye(2,dtype=complex)foriiinrange(len(one_qubits))]for_inrange(length):# sample random 1q-Clifford layerforj,qpairinenumerate(two_qubit_layer):# sample product of two 1q-Cliffords as 2q interger Cliffordsamples=rng.integers(NUM_1Q_CLIFFORD,size=2)# multiply unitaries for the 1Q cliffords we samplednp.dot(np.kron(oneq_cliff_mats[samples[1]],oneq_cliff_mats[samples[0]]),circs_2q[j],out=circs_2q[j],)forsample,qinzip(samples,qpair):circ._append(_to_gate_1q(sample),(circ.qubits[q],),())fork,qinenumerate(one_qubits):sample=rng.integers(NUM_1Q_CLIFFORD)circ._append(_to_gate_1q(sample),(circ.qubits[q],),())np.dot(oneq_cliff_mats[sample],circs_1q[k],out=circs_1q[k])forbarrier_instinbarrier_inst_lst:circ._append(barrier_inst)# add two qubit gatesforj,qpairinenumerate(two_qubit_layer):sample=rng.integers(len(two_qubit_gates))circ._append(two_qubit_gates[sample],tuple(circ.qubits[q]forqinqpair),())np.dot(two_q_gate_mats[sample],circs_2q[j],out=circs_2q[j])# TODO: add dd if necessaryfork,qinenumerate(one_qubits):# TODO: add dd if necessary# if there is a min_delay, just need# to add to one of the qubitsifmin_delayisnotNoneandk==0:circ.delay(min_delay,q)forbarrier_instinbarrier_inst_lst:circ._append(barrier_inst)# add the last inverse# invert the unitary matrix and transpile to a proper# circuitforj,qpairinenumerate(two_qubit_layer):qc_tmp=QuantumCircuit(2)qc_tmp.unitary(circs_2q[j].conjugate().transpose(),[0,1],label="test")qc_tmp=pass_manager_2q.run(qc_tmp)circ._append(qc_tmp.to_instruction(),tuple(circ.qubits[q]forqinqpair),())fork,qinenumerate(one_qubits):qc_tmp=QuantumCircuit(1)qc_tmp.unitary(circs_1q[k].conjugate().transpose(),[0],label="test")qc_tmp=pass_manager_1q.run(qc_tmp)circ._append(qc_tmp.to_instruction(),(circ.qubits[q],),())returncircdef_transpiled_circuits(self)->List[QuantumCircuit]:"""Return a list of experiment circuits, transpiled."""transpiled=[_decompose_clifford_ops(circ,True)forcircinself.circuits()]returntranspileddef_metadata(self):metadata=super()._metadata()metadata["two_qubit_layers"]=self.experiment_options.two_qubit_layersreturnmetadata
[docs]@classmethoddeffrom_config(cls,config:Union[ExperimentConfig,Dict])->"LayerFidelity":"""Initialize an experiment from experiment config"""ifisinstance(config,dict):config=ExperimentConfig(**config)ret=cls(*config.args,**config.kwargs)ifconfig.run_options:ret.set_run_options(**config.run_options)returnret