Creating a custom hardware model¶

In this tutorial, we show how to define one's own hardware model, with gates specified by Kraus operators (learn more about them in this tutorial), and predefined environmental noise.

As a concrete example, we consider a hardware with three types of gates:

  • Foo: a single-qubit gate with ideal unitary matrix $iZ$ (with $Z$ the Pauli Z matrix)
  • Bar: a parametric two-qubit gate, with ideal unitary matrix $\left[\begin{array}{cccc} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & e^{i\theta} & 0\\ 0 & 0 & 0 & 1 \end{array}\right]$
  • RX: a parametric single-qubit gate (the usual X rotation)

Circuit creation¶

In the following cell, we define the Foo and Bar gates:

In [1]:
import numpy as np
from qat.lang.AQASM.gates import AbstractGate

# Foo gate
foo_mat = np.array([[1j, 0], [0, -1j]], dtype=np.complex_)
# Foo has no parameters (hence the empty list) and arity 1
# Note: at the circuit-writing stage, we do not need to define the 'matrix_generator',
# but we nevertheless specify it as it can be useful for fidelity computations
Foo = AbstractGate("Foo", [], 1, matrix_generator=lambda foo_mat=foo_mat: foo_mat)

# Bar gate
bar_generator = lambda theta : np.array([[1, 0, 0, 0],
                                         [0, 1, 0, 0],
                                         [0, 0, np.exp(theta*1j), 0],
                                         [0, 0, 0, 1.]],
                                        dtype=np.complex_)
Bar = AbstractGate("Bar", [float], 2, 
                   matrix_generator=lambda theta, mat_gen=bar_generator: mat_gen(theta))

We now create our quantum program:

In [2]:
from qat.lang.AQASM import Program, RX
prog = Program(default_gate_set=False)
reg = prog.qalloc(2)
prog.apply(Foo(), reg[0])
prog.apply(Bar(np.pi/4.), reg)
prog.apply(RX(np.pi/8.), reg[1])
circ = prog.to_circ()

circ.display()
No description has been provided for this image

Take a look at this more advanced tutorial to learn more about custom gate sets.

An example of custom hardware model¶

In the following cell, we create a hardware model containing information about the hardware's gates and environment:

  • the gates: gate times, and actual quantum channels for each gate. Here, we describe quantum channels using Kraus operators:

  • Foo: this gate is described by two Kraus operators $E_0 = \sqrt{p} iZ$ and $E_1 = \sqrt{1-p} X$, where $X$ is the Pauli X matrix. Here, $p=0.75$. It takes 10 units of time to be executed.

  • Bar: this gate is described by its ideal unitary matrix. It takes $\theta/5+5$ units of time to be executed

  • RX: this gate produces a slight over rotation compared to the ideal gate: $\theta+0.02$ for qubit 0, $\theta+0.04$ for qubit 1, but is still unitary. It takes $\theta/10$ units of time to be executed.

  • the environment: we assume that the environment is described by predefined noise models such as amplitude damping (with $T_1 \approx 200$ units of time) or pure dephasing (with $T_\varphi \approx 100$ units of time), as well as a custom noise with an exponential decay law. These noise models apply only on idle qubits, and generally differ for each qubit.

In [3]:
import numpy as np
from qat.hardware import HardwareModel, GatesSpecification
from qat.quops import QuantumChannelKraus, ParametricAmplitudeDamping, ParametricPureDephasing
    
gate_times = {
                "Foo": 10,
                "Bar": lambda theta: theta/5.+5.,
                "RX": lambda theta: theta/10.,
             }

# specify the Kraus operators
# assuming "Foo" and "Bar" are identical for all qubits/qubit pairs, and "RX" depends on the qubit
foo_mat = np.array([[1j, 0], [0, -1j]], dtype=np.complex_)
quantum_channels = {
     "Foo": QuantumChannelKraus([np.sqrt(0.75)*foo_mat,
                                 np.sqrt(0.25)*np.array([[0,1],[1,0]],
                                                        dtype = np.complex_)]),
     "Bar": lambda theta: QuantumChannelKraus([np.array([[1, 0, 0, 0],
                                                       [0, 1, 0, 0],
                                                       [0, 0, np.exp(theta*1j), 0],
                                                       [0, 0, 0, 1.]],   dtype = np.complex_)]),
     "RX" : {
         0: lambda theta: QuantumChannelKraus([np.array([[np.cos(theta/2+.01), -1j*np.sin(theta/2+.01)],
                                                          [-1j*np.sin(theta/2+.01), 1j*np.cos(theta/2+.01)]],
                                                         dtype = np.complex_)]),
         1: lambda theta: QuantumChannelKraus([np.array([[np.cos(theta/2+.02), -1j*np.sin(theta/2+.02)],
                                                          [-1j*np.sin(theta/2+.02), 1j*np.cos(theta/2+.02)]],
                                                         dtype = np.complex_)]),
     }

}

gates_spec = GatesSpecification(gate_times, quantum_channels=quantum_channels)

########################
## environment #########
########################
# custom noise
def my_custom_noise(idling_time):
    error_prob = 1 - np.exp(-idling_time / 400.)
    return QuantumChannelKraus([np.sqrt(1-error_prob)*np.identity(2), np.sqrt(error_prob)* np.array([[0,1],[1,0]])],
                               name = "MyCustomNoise")

# we assume that each qubit experiences a different AD/PD noise, and the same custom noise    
idle_noise = {
   0: [ParametricAmplitudeDamping(T_1=210), ParametricPureDephasing(T_phi=105), my_custom_noise],
   1: [ParametricAmplitudeDamping(T_1=208), ParametricPureDephasing(T_phi=95), my_custom_noise],
   2: [ParametricAmplitudeDamping(T_1=199), ParametricPureDephasing(T_phi=101), my_custom_noise],
}
# note: in the case of identical noise parameters for each qubit, we could have written:
# idle_noise = [ParametricAmplitudeDamping(T_1=200), ParametricPureDephasing(T_phi=100), my_custom_noise]

hardware_model = HardwareModel(gates_spec, None, idle_noise)