Source code for pytket.extensions.qiskit.backends.crosstalk_model
# Copyright 2019-2024 Quantinuum## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.fromdataclassesimportdataclassfromtypingimportOptionalimportnumpyasnpfromqiskit_aer.noiseimportNoiseModel# type: ignorefromqiskit_aer.noise.errors.standard_errorsimport(# type: ignoreamplitude_damping_error,phase_damping_error,)fromscipy.linalgimportfractional_matrix_power# type: ignorefrompytket.backends.backendinfoimportBackendInfofrompytket.circuitimport(Circuit,Command,Node,Op,OpType,Qubit,Unitary1qBox,Unitary2qBox,Unitary3qBox,)frompytket.extensions.qiskit.qiskit_convertimport_gate_str_2_optype@dataclassclassFractionalUnitary:""" Wrapper for a fractional unitary gate. :param cmd: the fractional UnitaryBox wrapped in a pytket Command :param n_fractions: the number of fractional gates used to compose the original unitary gate. """cmd:Commandn_fractions:float@dataclassclassNoiseGate:""" Wrapper for a gate that simulates noise :param cmd: gate wrapped in a pytket Command :param type: one of zz_crosstalks, single_q_phase, two_q_induced_phase and non_markovian. """cmd:Commandtype:strInstruction=FractionalUnitary|Command|NoiseGateSlice=list[Instruction]EPS=1e-9
[docs]@dataclassclassCrosstalkParams:""" Stores various parameters for modelling crosstalk noise :param zz_crosstalks: symmetric crosstalks between qubit pairs :param single_q_phase_errors: dict specify the single qubit phase error on each qubit :param two_q_induced_phase_errors: keys of dictionary specify the control and target qubit index, while the values are tuples with the spectator qubit index and the amount of phase error to be applied. :param non_markovian_noise: List storing the non-Markovian noise parameters. Each tuple in the list contains the qubit index and the zx, zz noise parameters. :param virtual_z: If True, then don't break any single qubit Z gate into unitary fractions, instead add the full unitary. :param N: hyperparameter specifies splices per second. 1/N must divide all gate times. :param gate_times: python dict to store the gate time information. :param phase_damping_error: dict specify amplitude phase damping error on each qubit :param amplitude_damping_error: dict pecify amplitude damping error on each qubit """zz_crosstalks:dict[tuple[Qubit,Qubit],float]single_q_phase_errors:dict[Qubit,float]two_q_induced_phase_errors:dict[tuple[Qubit,Qubit],tuple[Qubit,float]]non_markovian_noise:list[tuple[Qubit,float,float]]virtual_z:boolN:floatgate_times:dict[tuple[OpType,tuple[Qubit,...]],float]phase_damping_error:dict[Qubit,float]amplitude_damping_error:dict[Qubit,float]
[docs]defget_noise_model(self)->NoiseModel:"""Construct a NoiseModel from phase_damping_error and amplitude_damping_error"""noise_model=NoiseModel()forq,phaseinself.phase_damping_error.items():noise_model.add_quantum_error(phase_damping_error(phase/self.N),["unitary"],[q.index[0]])forq,ampinself.amplitude_damping_error.items():noise_model.add_quantum_error(amplitude_damping_error(amp/self.N),["unitary"],[q.index[0]],warnings=False,)returnnoise_model
classNoisyCircuitBuilder:"""Builder used to generate a noisy circuit"""Ibox=Unitary1qBox(np.eye(2))# type: ignoreSUPPORTED_TYPES={OpType.Unitary1qBox,OpType.Unitary2qBox,OpType.Unitary3qBox,OpType.Measure,OpType.Reset,}def__init__(self,circ:Circuit,ct_params:CrosstalkParams,)->None:"""Construct a builder to generate noisy circuit :param circ: the original circuit. :param N: hyperparameter N :param ct_params: crosstalk parameters. """self.circ=circself.all_qubits=set(circ.qubits)self.N=ct_params.Nself.ct_params=ct_paramsself.two_level_map={}fori,(q,_,_)inenumerate(self.ct_params.non_markovian_noise):two_level_q=Qubit("two-level",i)self.two_level_map[q]=two_level_qself.reset()defreset(self)->None:"""Clear the build cache"""self._slices:list[Slice]=[]@staticmethoddef_get_qubits(inst:Instruction)->list[Qubit]:ifisinstance(inst,Command):returninst.qubitselse:returninst.cmd.qubitsdef_append(self,inst:Instruction,frontier:dict[Qubit,int],)->None:"""Append a command to the splices in Tetris style"""args=self._get_qubits(inst)pivot_q=max(args,key=lambdaq:frontier[q])slice_idx=frontier[pivot_q]# add cmd to the idx_th slicen_slices=len(self._slices)assertslice_idx<=n_slicesifslice_idx==n_slices:self._slices.append([inst])else:self._slices[slice_idx].append(inst)# update frontierforqinargs:frontier[q]=slice_idx+1def_fill_gaps(self,frontier:dict[Qubit,int])->None:"""Fill the gaps in the slices with identity `Unitary1qBox`es"""foridx,sinenumerate(self._slices):slice_qubits=set().union(*[self._get_qubits(inst)forinstins])gap_qs=self.all_qubits-slice_qubitsforqingap_qs:# only fill up to the frontierifidx<frontier[q]:s.append(Command(self.Ibox,[q]))defsort_and_fill_gaps(self)->None:"""Sort splices so each slice only contains independent instructions"""old_slices=self._slices.copy()self._slices=[]frontier={q:0forqinself.all_qubits}forsinold_slices:forinstins:self._append(inst,frontier)self._fill_gaps(frontier)@staticmethoddef_get_ubox(u:np.ndarray)->Unitary1qBox|Unitary2qBox|Unitary3qBox:"""Return a UnitaryxqBox for a given unitary"""ifu.shape[0]==2:returnUnitary1qBox(u)ifu.shape[0]==4:returnUnitary2qBox(u)ifu.shape[0]==8:returnUnitary3qBox(u)raiseValueError(f"Unsupported unitary shape: {u.shape}")defunitary_factorisation(self)->None:"""For each unitary U with time D, factorise it into N*D unitaries u_i, such that u_0;u1;...;u_(N*D-1) = U. Store the factorised circuit as a list of slices """forcmdinself.circ:ifcmd.op.typein[OpType.Measure,OpType.Reset]:self._slices.append([cmd])else:gt:Optional[float]ifself.ct_params.virtual_zandcmd.op.type==OpType.Z:gt=1/self.Nelse:gt=self.ct_params.gate_times.get((cmd.op.type,tuple(cmd.qubits)))ifgtisNone:raiseValueError(f"No gate time for OpType {cmd.op.type} on qubits {cmd.qubits}")u=cmd.op.get_unitary()n_fractions=round(self.N*gt)ifabs((self.N*gt)-n_fractions)>1e-9:raiseValueError(f"Command {cmd} cannot be factorised into equal slices")power=1/n_fractionsu_i=fractional_matrix_power(u,power)for_inrange(n_fractions):u_i_box=self._get_ubox(u_i)self._slices.append([FractionalUnitary(Command(u_i_box,cmd.args),n_fractions)])def_add_zz_crosstalks(self,noise_slice:Slice)->None:for(q0,q1),zzinself.ct_params.zz_crosstalks.items():ifq0inself.all_qubitsandq1inself.all_qubits:Z=zz/self.Nifabs(Z)>EPS:noise_slice.append(NoiseGate(Command(Op.create(OpType.ZZPhase,Z),[q0,q1]),"zz_crosstalks",))def_add_single_q_phase(self,noise_slice:Slice)->None:forq,zinself.ct_params.single_q_phase_errors.items():ifqinself.all_qubits:Z=z/self.Nifabs(Z)>EPS:noise_slice.append(NoiseGate(Command(Op.create(OpType.Rz,Z),[q]),"single_q_phase"))def_add_two_q_induced_phase(self,unitary_slice:Slice,noise_slice:Slice)->None:forinstinunitary_slice:if(isinstance(inst,FractionalUnitary)andinst.cmd.op.type==OpType.Unitary2qBox):qubits=inst.cmd.qubitsvalue=self.ct_params.two_q_induced_phase_errors.get((qubits[0],qubits[1]))ifvalueisNone:# Don't add and error if the two-q interaction is not specifiedcontinueZ=value[1]/inst.n_fractionsifabs(Z)>EPS:noise_slice.append(NoiseGate(Command(Op.create(OpType.Rz,Z),[value[0]]),"two_q_induced_phase",))def_add_non_markovian(self,noise_slice:Slice)->None:forq,zx,zzinself.ct_params.non_markovian_noise:two_level_q=self.two_level_map[q]ZX=zx/self.NZZ=zz/self.Nifabs(ZZ)>EPS:noise_slice.append(NoiseGate(Command(Op.create(OpType.ZZPhase,ZZ),[two_level_q,q]),"non_markovian",))ifabs(ZX)>EPS:noise_slice.extend([NoiseGate(Command(Op.create(OpType.H),[q]),"non_markovian",),NoiseGate(Command(Op.create(OpType.ZZPhase,ZX),[two_level_q,q]),"non_markovian",),NoiseGate(Command(Op.create(OpType.H),[q]),"non_markovian",),])defadd_noise(self)->None:"""Add noise gates between slices"""i=1base_noise_slice:Slice=[]self._add_zz_crosstalks(base_noise_slice)self._add_single_q_phase(base_noise_slice)self._add_non_markovian(base_noise_slice)whilei<len(self._slices):noise_slice:Slice=[]ifself.circ.n_qubits>2:self._add_two_q_induced_phase(self._slices[i-1],noise_slice)noise_slice.extend(base_noise_slice)self._slices.insert(i,noise_slice)i=i+2defbuild(self)->None:"""Build the noisy circuit as slices"""self.reset()self.unitary_factorisation()self.sort_and_fill_gaps()self.add_noise()defget_circuit(self)->Circuit:"""Convert the slices into a circuit"""d=Circuit()forqinself.circ.qubits:d.add_qubit(q)forqinself.two_level_map.values():d.add_qubit(q)forbinself.circ.bits:d.add_bit(b)forsinself._slices:forinstins:ifisinstance(inst,Command):d.add_gate(inst.op,inst.args)else:d.add_gate(inst.cmd.op,inst.cmd.args)returnddefget_slices(self)->list[Slice]:"""Return the internally stored slices"""returnself._slicesdefget_gate_times_from_backendinfo(backend_info:BackendInfo,)->dict[tuple[OpType,tuple[Qubit,...]],float]:"""Convert the gate time information stored in a `BackendInfo` into the format required by `NoisyCircuitBuilder`"""if("characterisation"notinbackend_info.miscor"GateTimes"notinbackend_info.misc["characterisation"]):raiseValueError("'GateTimes' is not present in the provided 'BackendInfo'")gate_times:dict[tuple[OpType,tuple[Qubit,...]],float]={}forgtinbackend_info.misc["characterisation"]["GateTimes"]:# GateTimes are nanosecondsgate_times[_gate_str_2_optype[gt[0]],tuple([Node(q)forqingt[1]])]=(gt[2]/1e9)returngate_times