Source code for qiskit_experiments.test.t2hahn_backend

# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# 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.
"""
T2HahnBackend class.
Temporary backend to be used for t2hahn experiment
"""
import copy
from typing import List, Optional, Sequence, Union

import numpy as np

from qiskit import QiskitError, QuantumCircuit
from qiskit.circuit import Delay, Reset, Parameter
from qiskit.circuit.library import Measure, RZGate, SXGate, XGate
from qiskit.dagcircuit import DAGCircuit
from qiskit.providers import BackendV2, Job, Options
from qiskit.transpiler import InstructionProperties, PassManager, Target, TransformationPass
from qiskit.utils.units import apply_prefix

from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, ReadoutError, RelaxationNoisePass, reset_error

from qiskit_experiments.test.utils import FakeJob


class ResetQubits(TransformationPass):
    """Pass to inject reset instructions for each qubit

    The resets are used to add qubit initialization error.
    """

    def run(self, dag: DAGCircuit):
        new_dag = copy.deepcopy(dag)

        for qreg in new_dag.qregs.values():
            new_dag.apply_operation_front(Reset(), qreg, [])

        return new_dag


class QubitDrift(TransformationPass):
    """Pass to rotate qubits during delays

    This pass adds rotations that mimic the qubit being detuned from the drive
    frequency (while assuming that the gate times are negligible; rotations are
    only added for delays).
    """

    def __init__(self, qubit_frequencies: Sequence[float], dt: float):
        super().__init__()
        self.qubit_frequencies = qubit_frequencies
        self.dt = dt

    def run(self, dag: DAGCircuit):
        qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}

        new_dag = dag.copy_empty_like()

        for node in dag.topological_op_nodes():
            new_dag.apply_operation_back(node.op, node.qargs, node.cargs)

            if node.name == "delay":
                q0 = qubit_indices[node.qargs[0]]
                if self.qubit_frequencies[q0] is None:
                    continue
                if node.op.unit == "dt":
                    duration = node.op.duration * self.dt
                elif node.op.unit != "s":
                    duration = apply_prefix(node.op.duration, node.op.unit)
                angle = 2 * np.pi * self.qubit_frequencies[q0] * duration
                angle = angle % (2 * np.pi)
                new_dag.apply_operation_back(RZGate(angle), [node.qargs[0]], [])

        return new_dag


[docs] class T2HahnBackend(BackendV2): """ A simple and primitive backend, to be run by the T2Hahn tests """ def __init__( self, t2hahn: Union[float, Sequence[float]] = float("inf"), frequency: Union[float, Sequence[float]] = 0.0, initialization_error: Union[float, Sequence[float]] = 0.0, readout0to1: Union[float, Sequence[float]] = 0.0, readout1to0: Union[float, Sequence[float]] = 0.0, seed: int = 9000, dt: float = 1 / 4.5e9, num_qubits: Optional[int] = None, ): """ Initialize the T2Hahn backend """ super().__init__( name="T2Hahn_simulator", backend_version="0", ) for arg in (t2hahn, frequency, initialization_error, readout0to1, readout1to0): if isinstance(arg, Sequence): if num_qubits is None: num_qubits = len(arg) elif len(arg) != num_qubits: raise ValueError( f"Input lengths are not consistent: {num_qubits} != {len(arg)}" ) if num_qubits is None: num_qubits = 1 self._t2hahn = t2hahn if isinstance(t2hahn, Sequence) else [t2hahn] * num_qubits self._frequency = frequency if isinstance(frequency, Sequence) else [frequency] * num_qubits self._initialization_error = ( initialization_error if isinstance(initialization_error, Sequence) else [initialization_error] * num_qubits ) self._readout0to1 = ( readout0to1 if isinstance(readout0to1, Sequence) else [readout0to1] * num_qubits ) self._readout1to0 = ( readout1to0 if isinstance(readout1to0, Sequence) else [readout1to0] * num_qubits ) self._seed = seed self._target = Target(dt=dt, num_qubits=num_qubits) for instruction in (Measure(), Reset(), RZGate(Parameter("angle")), SXGate(), XGate()): self.target.add_instruction( instruction, properties={(q,): InstructionProperties(duration=0) for q in range(num_qubits)}, ) self.target.add_instruction(Delay(Parameter("duration"))) @property def target(self) -> Target: return self._target @property def max_circuits(self) -> None: return None @classmethod def _default_options(cls) -> Options: return Options()
[docs] def run( self, run_input: Union[QuantumCircuit, List[QuantumCircuit]], shots: int = 1024, **options ) -> Job: passes = [] if isinstance(run_input, QuantumCircuit): circuits = [run_input] else: circuits = run_input for circuit in circuits: if circuit.num_qubits > self.num_qubits: raise QiskitError( f"{self.__class__} can only run circuits that match its num_qubits" ) noise_model = NoiseModel() if self._initialization_error is not None: passes.append(ResetQubits()) for qubit, error in enumerate(self._initialization_error): if error is None: continue noise_model.add_quantum_error(reset_error(1 - error, error), ["reset"], [qubit]) if any(self._readout0to1) or any(self._readout1to0): for qubit, (err0to1, err1to0) in enumerate(zip(self._readout0to1, self._readout1to0)): error = ReadoutError([[1 - err0to1, err0to1], [err1to0, 1 - err1to0]]) noise_model.add_readout_error(error, [qubit]) if any(t2 != float("inf") for t2 in self._t2hahn): # Make T1 huge so only T2 matters passes.append( RelaxationNoisePass([t * 100 for t in self._t2hahn], self._t2hahn, self.dt, Delay) ) if any(self._frequency): passes.append(QubitDrift(self._frequency, self.dt)) pm = PassManager(passes) new_circuits = pm.run(circuits) sim = AerSimulator(noise_model=noise_model, seed_simulator=self._seed) job = sim.run(new_circuits, shots=shots) return FakeJob(self, job.result())