<jupyter_start><jupyter_text>Creating a custom hardware modelIn 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](quantum_channels.ipynb)), 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 creationIn the following cell, we define the Foo and Bar gates:<jupyter_code>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))<jupyter_output><empty_output><jupyter_text>We now create our quantum program:<jupyter_code>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()

%qatdisplay circ<jupyter_output><empty_output><jupyter_text>Take a look at [this more advanced tutorial](../lang/custom_gate_set.ipynb) to learn more about custom gate sets.  An example of custom hardware modelIn 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.<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>Noisy simulationIn the following cell, we show how to use the hardware model defined above in a noisy simulation:<jupyter_code>from qat.noisy import NoisyQProc


# we choose the deterministic method to simulate the circuit
# this method is costly in terms of memory as it is based on the density matrix
qpu = NoisyQProc(hardware_model=hardware_model, sim_method="deterministic")

job = circ.to_job(nbshots=10)

# we submit the job
result = qpu.submit(job)

# this simulates the measured output state of a quantum computer (for 5 successive preparations)
for sample in result:
    print("State %s, probability %s, err %s"%(sample.state, sample.probability, sample.err))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1220547343772867, 'fraction_numerical': 0.026489097029123373, 'mean_word_length': 2.9096109839816933, 'pattern_counts': {'":': 5, '<': 17, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '211089', 'license': 'Apache-2.0', 'n_tokens_mistral': 2181, 'n_tokens_neox': 1984, 'n_words': 665, 'path': 'misc/notebooks/tutorials/noisy_simulation/create_own_hardware_model.ipynb', 'repo_name': 'wmkirby1/CS-VQE', 'size': '6833'}