```
# SETUP (common to all tutorials)
import fermioniq
client = fermioniq.Client() # Initialize the client (assumes environment variables are set)
import rich # Module for pretty printing
import nest_asyncio # Necessary only for Jupyter, can be omitted in .py files
nest_asyncio.apply()
```

# 5) Noisy emulation#

To perform a noisy emulation, you need to specify a noise model. There are two ways to do this:

Defining your own noise model. Explained in this tutorial

Choose a pre-configured noise model supported by the emulator. Currently Unavailable

**Defining a custom noise model**

Broadly speaking, a noise model is built in three steps:

Build a

`NoiseModel`

objectDefine

`NoiseChannels`

Specify when the

`NoiseChannels`

should be applied

A

`NoiseModel`

is defined for a list of qubits (Qiskit or Cirq qubit objects).

**Important note**: These qubits should match the qubits in the circuit. Hence it is best to first create a circuit in Qiskit/Cirq.

```
from qiskit import QuantumCircuit
from fermioniq import NoiseModel
# Create a 3-qubit circuit for which the noise model is defined (gates can be added later)
qiskit_circuit = QuantumCircuit(3)
noise_model = NoiseModel(qubits=qiskit_circuit.qubits, name="example-qiskit-noise-model", description="An example noise model for 3 qubits")
```

`NoiseChannel`

s are imported from the client. Several common channels can be defined easily, and custom channels can be specified via their Kraus operators or Pauli channel descriptions.

```
# These are all available channels
from fermioniq import DepolarizingChannel, PhaseDampingChannel, AmplitudeDampingChannel, PhaseAmplitudeDampingChannel, BitFlipChannel, PauliChannel, KrausChannel
import numpy as np
# Examples of channel instances -- see class descriptions for details on the input parameters.
d = DepolarizingChannel(p=0.3, num_qubits=2)
pd = PhaseDampingChannel(gamma_pd=(gamma_pd:=0.3))
ad = AmplitudeDampingChannel(gamma_ad=0.3, excited_state_population=0.1)
pad = PhaseAmplitudeDampingChannel(gamma_pd=0.3, gamma_ad=0.3, excited_state_population=0.1)
b = BitFlipChannel(p0=0.2, p1=0.4)
# Example of a custom Krauss channel, in this case re-implementing the phase damping channel using Kraus operators
k_0 = np.array([[1, 0], [0, np.sqrt(1-gamma_pd)]])
k_1 = np.array([[0,0], [0, np.sqrt(gamma_pd)]])
kraus_pd = KrausChannel(kraus_ops=[k_0, k_1])
# Example of a single-qubit depolarizing channel using Pauli strings.
# Note that the PauliChannel constructor takes a dictionary of Pauli strings and their *probability* of being applied.
p = 0.2
pauli_d = PauliChannel(pauli_probs={"I": 1 - 3*p/4, "X": p/4, "Y": p/4, "Z": p/4})
```

Dict representations of the channel can be printed if needed.

```
rich.print(d.dict())
rich.print(pd.dict())
rich.print(ad.dict())
rich.print(pad.dict())
rich.print(b.dict())
rich.print(kraus_pd.dict())
rich.print(pauli_d.dict())
```

Add

`NoiseChannels`

to the`NoiseModel`

by specifying the*conditions*under which they are triggered. A condition is a combination of a gate and the qubits that that gate acts upon.

For instance, a very simple noise model could be one with the following triggers and channels:

Upon application of any 2-qubit gate to any pair of qubits (

**trigger**), apply 2-qubit depolarizing noise to those**same**qubits*after*the gate is applied (**noise channel**).Upon application of any 1-qubit gate to any qubit (

**trigger**), apply single-qubit depolarizing noise to that**same**qubit*before*the gate is applied (**noise channel**).

**Important note**: noise should be specified for all gates (even if it is via a generic rule using`ANY`

), and readout errors should always be specified for all qubits. This restriction can be dropped later by setting a config flag.

```
from fermioniq import ANY
# Add two-qubit gate noise
noise_model.add(gate_name=ANY, gate_qubits=2, channel=d, channel_qubits="same", when="post")
# Add one-qubit gate noise
d_one_qubit = DepolarizingChannel(p=0.3, num_qubits=1)
noise_model.add(gate_name=ANY, gate_qubits=1, channel=d_one_qubit, channel_qubits="same", when="pre")
# We add trivial readout error, both the probability of misreading 0 (first number), and that of misreading 1 (second number) are 0
noise_model.add_readout_error(qubit = ANY, p0 = 0, p1 = 0)
```

**Noisy Emulation**

To run noisy emulation simply add the **NoiseModel** to the emulator job.

**Important note**: To get settings in **standard_config** that are sensible for mixed state emulation, set **noise** **=** **True**.

```
from fermioniq.config.defaults import standard_config
# Add some gates to the circuit
qiskit_circuit.h(0)
qiskit_circuit.x(1)
qiskit_circuit.cx(0, 1)
qiskit_circuit.cx(1, 2)
qiskit_circuit.cx(0, 1)
print(qiskit_circuit)
# Create a standard configuration for a noisy emulation of the circuit
noisy_config = standard_config(qiskit_circuit, noise=True)
rich.print(noisy_config)
# Adding the noise model to the emulator job will automatically configure it for a noisy emulation
emulator_job = fermioniq.EmulatorJob(circuit=qiskit_circuit, config=noisy_config, noise_model=noise_model)
# Note that the output will be uninteresting, since we didn't add any gates to the circuit yet!
result = client.schedule_and_wait(emulator_job)
rich.print(result)
```

**Testing your NoiseModel**

We can apply a

`NoiseModel`

to a circuit to get a full overview of the resulting noisy circuit by using`on_circuit(circuit)`

. This returns a dictionary with two keys:

`noise_by_gate`

: A list of dictionaries, one for each gate in the circuit, giving the gate and the noise channels that are applied before and after the gate.`missing_noise`

: A list of dictionaries, one for each gate in the circuit for which no noise channels were found, giving the gate, the position in the circuit, and the trace of the lookup process (useful for debugging).

We can inspect the channels

**triggered**by a specific gate on specific qubits by using`get_noise_channels(gate_name, gate_qubits, trace=False)`

. By setting the`trace`

flag to`True`

, the function also returns the lookup trace: a dictionary describing how the noise model arrived at the channels that it did (i.e. where it did and did not find matches). This is useful for debugging.

```
# Define a circuit
qiskit_circuit.h(0)
qiskit_circuit.cx(0, 1)
qiskit_circuit.cx(1, 0)
qiskit_circuit.h(2)
qiskit_circuit.cx(1, 2)
qubits = qiskit_circuit.qubits
# 1: Full overview of the noisy circuit
noise_effects = noise_model.on_circuit(qiskit_circuit)
rich.print("Noise effects:")
rich.print(noise_effects)
# 2: Noise for specific gate and qubits
channels, trace = noise_model.get_noise_channels(gate_name="h", gate_qubits=[qubits[0]], trace=True)
rich.print("Lookup trace for H on q_0:")
rich.print(trace)
rich.print("(matches rule 1: H on q_0)")
channels, trace = noise_model.get_noise_channels(gate_name="cx", gate_qubits=[qubits[1], qubits[2]], trace=True)
rich.print("Lookup trace for CX on q_1 and q_2:")
rich.print(trace)
rich.print("(matches rule 5: CX on any 2 qubits)")
```

**Full overview of options and behaviour**

We will now explain in more detail:

Which

**triggers**can be definedHow a

`NoiseModel`

handles multiple definitions of channels**triggered**by the same gate and qubits.Which errors might be thrown when building a

`NoiseModel`

.

**Triggers**

Noise models can be as complicated or as general as desired. The only restriction (currently) is that the channels act on either one or two qubits.

The `add`

function takes the following arguments:

`gate_name`

: The name of the gate that triggers the noise. Can be either a gate name for a Qiskit or Cirq circuit as a string, e.g. ‘cx’, ‘h’, ‘CNOT’, ‘H’, ‘Y’, or`ANY`

(see below);`gate_qubits`

: The qubits acted upon by the gate that triggers the noise. Can be a particular set of qubits such as (‘q1’, ‘q2’), ‘q0’, or ‘q3’, or an integer (1 or 2) specifying the number of qubits the gate acts on;`channel`

: The channel that is applied. Can be any one of the fermioniq noise channels (see above);`channel_qubits`

: The qubits that the channel acts on. Can be a particular set of qubits, an integer (1 or 2) specifying the number of qubits, or ‘same’ to apply the channel to the same qubits as the gate (i.e.`gate_qubits`

);`when`

: When the noise is applied. Can be either ‘pre’ or ‘post’, determining whether the channel should be applied before or after the gate.

(The `ANY`

constant from `fermioniq`

can be used to match to any gate or set of qubits)

Above, `gate_name`

and `gate_qubits`

together specify the trigger, and `channel`

and `channel_qubits`

specify the noise channel that should be applied before or after the gate in response to this trigger.

Multiple channels can be added for the same **trigger**, in this case the order of application is:

Channels in “pre”, in the order in which they were added (

`when='pre'`

).The gate itself.

Channels in “post”, in the order in which they were added (

`when='post`

).

For defining **triggers**, all gate names in Cirq and Qiskit are available (depending on which type of circuit is being used).

For instance, suppose we want to construct a noise model with the following ‘rules’:

Whenever a

`H`

gate acts on qubit`q0`

, apply a`phase damping channel`

to`q0`

, then an`amplitude damping channel`

to`q1`

(modelling some kind of cross-talk).Whenever a

`CX`

gate acts on qubits`q0`

and`q1`

, apply a`depolarizing channel`

to those qubits, as well as an`amplitude damping channel`

to qubit`q0`

(the control qubit)*before the gate*.Whenever a

`CX`

gate acts on qubits`q1`

and`q0`

, apply a`depolarizing channel`

to those qubits, as well as an`amplitude damping channel`

to qubit`q1`

(the control qubit)*before the gate*. Note that this is the same as the previous rule, but with the qubits swapped.Whenever a

`H`

gate acts on`any qubit`

, apply a`phase-amplitude damping channel`

to that qubit.Whenever

`any`

gate acts on`any two`

qubits, apply a`depolarizing channel`

to those same qubits.Whenever

`any`

gate acts on`any one or two`

qubits, apply a`phase damping channel`

to those same qubits.Qubit

`q0`

has readout error where 0 is flipped to 1 with probability 0.3, and 1 is flipped to 0 with probability 0.6.All other qubits have readout errors where 0 is flipped to 1 with probability 0.1, and 1 is flipped to 0 with probability 0.2.

Note that rules 1, 2, and 3 are specific in terms of both the gate and the qubits that it acts on. Rule 4 is specific in terms of the gate, but generic in terms of the qubits. Rules 5 and 6 are generic in terms of both the gate and the qubits, with 5 specifiying a number of qubits, and 6 specifying ‘any’ qubits.

```
from fermioniq import NoiseModel, ANY
import qiskit
# Standard qiskit gate constructors are given here
from qiskit.circuit.library import standard_gates
# print(dir(standard_gates))
from qiskit.circuit.library import HGate, CXGate
# These strings correspond to those used in the qiskit 'circuit.add()' function, i.e. "h", "cx"
# Alternatively, they can be obtained from instructions in a qiskit QuantumCircuit
HGate_name = HGate().name
CXGate_name = CXGate().name
assert HGate_name == "h"
assert CXGate_name == "cx"
# Let's make an empty circuit for which the noise model will be defined
qiskit_circuit = qiskit.QuantumCircuit(3)
# Qubits on which the noise model acts
qubits = list(qiskit_circuit.qubits)
noise_model = NoiseModel(qubits=qubits, name="example-qiskit-noise-model", description="An example noise model for 3 qubits")
# Rule 1; first apply phase damping, then amplitude damping
noise_model.add(gate_name="h", gate_qubits=[qubits[0]], channel=pd, channel_qubits=[qubits[0]])
noise_model.add(gate_name="h", gate_qubits=[qubits[0]], channel=ad, channel_qubits=[qubits[1]])
# Rule 2.
noise_model.add(gate_name="cx", gate_qubits=qubits[0:2], channel=d, channel_qubits=qubits[0:2])
noise_model.add(gate_name="cx", gate_qubits=qubits[0:2], channel=ad, channel_qubits=[qubits[0]], when="pre")
# # Rule 3.
noise_model.add(gate_name="cx", gate_qubits=[qubits[1], qubits[0]], channel=d, channel_qubits="same") # channel_qubits="same" is an equivalent way to specify channel_qubits=gate_qubits
noise_model.add(gate_name="cx", gate_qubits=[qubits[1], qubits[0]], channel=ad, channel_qubits=[qubits[1]], when="pre")
# # Rule 4.
noise_model.add(gate_name="h", gate_qubits=1, channel=pad, channel_qubits="same")
# # Rule 5.
noise_model.add(gate_name=ANY, gate_qubits=2, channel=d, channel_qubits="same")
# # Rule 6.
noise_model.add(gate_name=ANY, gate_qubits=1, channel=kraus_pd, channel_qubits="same")
noise_model.add(gate_name=ANY, gate_qubits=2, channel=kraus_pd, channel_qubits="same")
# # Rule 7.
noise_model.add_readout_error(qubits[0], 0.3, 0.6)
# # Rule 8.
noise_model.add_readout_error(ANY, 0.1, 0.2)
```

**Custom Gates**

Additionally, **triggers** can be defined with custom gates (a `unitary`

in Qiskit, or `MatrixGate`

in Cirq). If you wish to create custom gates with their own labels, then you should add those labels to the noise model’s list of supported gates, just like in Qiskit. To do this, you can use the `add_supported_gate()`

method of the `NoiseModel`

class. This method takes two arguments: the gate name, and the number of qubits that the gate acts on. For instance, if you have a custom gate called `my_gate`

that acts on 2 qubits, you can add it to the noise model using `noise_model.add_supported_gate('my_gate', 2)`

.

```
from qiskit import QuantumCircuit
circuit = QuantumCircuit(2)
qubits = circuit.qubits
# Apply some custom unitaries (which for the purposes of the example are just the identity)
circuit.unitary(np.eye(2), [qubits[0]], label="U_0")
circuit.unitary(np.eye(2), [qubits[1]], label="U_1")
circuit.unitary(np.eye(4), [qubits[0], qubits[1]], label="U_01")
# Make a noise model
noise_model = NoiseModel(qubits=qubits, name="example-qiskit-noise-model", description="An example noise model for 2 qubits")
# Add the unitary names to the list of supported gates
noise_model.add_supported_gate(gate_name="U_0", n_qubits=1) # Acts on 1 qubit
noise_model.add_supported_gate(gate_name="U_1", n_qubits=1) # Acts on 1 qubit
noise_model.add_supported_gate(gate_name="U_01", n_qubits=2) # Acts on 2 qubits
# Add some noise for these gates
d1 = DepolarizingChannel(num_qubits=1, p=0.3)
noise_model.add(gate_name="U_0", gate_qubits=[qubits[0]], channel=d1, channel_qubits="same")
noise_model.add(gate_name="U_1", gate_qubits=[qubits[0]], channel=d1, channel_qubits="same")
noise_model.add(gate_name="U_01", gate_qubits=qubits[0:2], channel=d1, channel_qubits="same")
```

**Multiple matching triggers**

In many cases, triggers overlap. If a gate in the circuit matches a specific trigger (i.e. **cx** on qubits **(q0** **,** **q1)**), as well as a more general trigger (i.e. **any** gate on **2** qubits), only the noise from the more specific trigger is applied.

**Note**: This behavior is necessary for the noise model above to match rules **(1) - (8)**

Since it is not always obvious which trigger is ‘more general’, we specifically take the following view: gate triggers are more specific than qubit triggers – i.e. a trigger that matches a specific gate but generic qubits is considered *more specific* than a trigger than matches a generic gate but specific qubits. Precisely, the way that noise is applied to a circuit follows the lookup procedure in the flowchart below, which displays the decision process when a gate called <gate_name> is encountered in the circuit acting on <n> qubits with labels <qubits>.

**Errors**

When defining a `NoiseChannel`

, missing or inconsistent parameters return errors.

```
k_wrong = KrausChannel(kraus_ops=[np.array([[0, 0], [0, 0]])*np.sqrt(0.5), np.array([[0, 1], [1, 0]])*np.sqrt(0.5)])
```

```
pauli_wrong = PauliChannel(pauli_probs={"II": 0.5, "XI": 0.25, "YY": 0.2})
```

When adding noise to the `NoiseModel`

, errors are raised whenever things don’t add up:

```
# H gates only act on single qubits, so this trigger is invaild.
noise_model.add(gate_name="h", gate_qubits=[qubits[0],qubits[1]], channel=pd, channel_qubits=[qubits[0]])
```

```
# d is a DepolarizingNoise instance on 2 qubits, but only one target is given
noise_model.add(gate_name="h", gate_qubits=[qubits[0]], channel=d, channel_qubits=[qubits[0]])
```

```
# Noise is triggered by a single qubit, and attempted to be applied to that same qubit, but the channel is a 2-qubit channel.
noise_model.add(gate_name=ANY, gate_qubits=1, channel=d, channel_qubits="same")
```

```
# Noise is triggered by any gate on any single qubit. To apply a channel to the same qubits, it should be a single-qubit channel, or act on the same number of qubits as the gate (1 again, in this example).
noise_model.add(gate_name=ANY, gate_qubits=1, channel=d, channel_qubits="same")
```

An error is also raised if you try to add a noise rule for a gate name that is not in the list of supported gates:

```
noise_model.add(gate_name="'unsupported'", gate_qubits=qubits[0:2], channel=d, channel_qubits="same")
```

**Config and validation**

We mentioned earlier that noise should be specified for all gates and readout errors should always be specified for all qubits, otherwise an error will be thrown upon validation (creation of an emulator job). This restriction can be dropped by setting a config option. Specifically, setting the `validate_model`

flag of the `noise`

config dictionary to `False`

:

```
noisy_config["noise"]["validate_model"] = False
```