# 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.model_dump())
rich.print(pd.model_dump())
rich.print(ad.model_dump())
rich.print(pad.model_dump())
rich.print(b.model_dump())
rich.print(kraus_pd.model_dump())
rich.print(pauli_d.model_dump())
Add
NoiseChannels
to theNoiseModel
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 usingANY
), 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 usingon_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 thetrace
flag toTrue
, 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 defined
How 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’, orANY
(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 qubitq0
, apply aphase damping channel
toq0
, then anamplitude damping channel
toq1
(modelling some kind of cross-talk).Whenever a
CX
gate acts on qubitsq0
andq1
, apply adepolarizing channel
to those qubits, as well as anamplitude damping channel
to qubitq0
(the control qubit) before the gate.Whenever a
CX
gate acts on qubitsq1
andq0
, apply adepolarizing channel
to those qubits, as well as anamplitude damping channel
to qubitq1
(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 onany qubit
, apply aphase-amplitude damping channel
to that qubit.Whenever
any
gate acts onany two
qubits, apply adepolarizing channel
to those same qubits.Whenever
any
gate acts onany one or two
qubits, apply aphase 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