Noisy simulations on MIMIQ¶
This page provides detailed information on simulating noise in quantum circuits using MIMIQ.
Summary of noise functionality¶
Custom noise channels:
KrausMixedUnitary
Specialized noise channels:
DepolarizingDepolarizing1Depolarizing2PauliNoisePauliXPauliYPauliZAmplitudeDampingGeneralizedAmplitudeDampingPhaseAmplitudeDampingThermalNoiseProjectiveNoise
Note that the Reset type operations can also be thought of as noisy operations.
Coherent noise can be added by using any of the supported gates (e.g., Gate).
Noise channels come with the following methods:
krausmatrices()andkrausoperators()unitarymatrices()andunitarygates()(only for mixed-unitary)probabilities()(only for mixed-unitary)ismixedunitary()
To add noise channels to a circuit, you can use:
push()(like gates)add_noise()(to add noise to every instance of a gate)
To generate one sample of a circuit with mixed unitaries, use:
sample_mixedunitaries()
See below for further information. You can also run help(Circuit().sample_mixedunitaries).
Mathematical background¶
Kraus operators¶
Noise in a quantum circuit refers to any kind of unwanted interaction of the qubits with the environment (or with itself). Mathematically, this puts us in the framework of open systems and the state of the qubits now needs to be described in terms of a density matrix \(\rho\), which fulfills \(\rho = \rho^\dagger\) and \(\mathrm{Tr} (\rho) = 1\).
A quantum operation such as noise can then be described using the Kraus operator representation as:
We consider only completely positive and trace-preserving (CPTP) operations. In this case, the Kraus operators \(E_k\) can be any matrix as long as they fulfill the completeness relation:
Note that unitary gates \(U\) just correspond to a single Kraus operator, \(E_1 = U\).
When all Kraus operators are proportional to a unitary matrix, \(E_k = \alpha_k U_k\), this is called a mixed-unitary quantum operation and can be written as:
where \(p_k = |\alpha_k|^2\).
Such operations are easier to implement as we’ll see below.
Remarks:
Unitary gates \(U\) correspond to a single Kraus operator, \(E_1 = U\).
The number of Kraus operators depends on the noise considered.
For a given quantum operation \(\mathcal{E}\), the Kraus operator representation is not unique. One can change the basis of Kraus operators using a unitary matrix \(U\) as \(\tilde{E}_i = \sum_j U_{ij} E_j\).
We define a noise channel (or Kraus channel) as a quantum operation \(\mathcal{E}\) described by a set of Kraus operators as given above. A common way of modeling noisy quantum computers is by considering each operation \(O\) in the circuit as a noisy quantum operation \(\mathcal{E}_O\).
Evolution with noise¶
There are two common ways to evolve the state of the system when acting with Kraus channels:
Density matrix: If we use a density matrix to describe the qubits, then a Kraus channel can simply be applied by directly performing the matrix multiplications as:
\[\mathcal{E}(\rho) = \sum_k E_k \rho E_k^\dagger.\]The advantage of this approach is that the density matrix contains the full information of the system and we only need to run the circuit once. The disadvantage is that \(\rho\) requires more memory to be stored (\(2^{2N}\) as opposed to \(2^N\) for a state vector), so we can simulate fewer qubits.
Quantum trajectories: This method consists in simulating the evolution of the state vector \(|\psi_\alpha \rangle\) for a set of iterations \(\alpha = 1, \ldots, n\). In each iteration, a noise channel is applied by randomly selecting one of the Kraus operators according to some probabilities (see below) and applying that Kraus operator to the state vector. The advantage of this approach is that we need less memory since we work with a state vector. The disadvantage is that we need to run the circuit many times to collect samples (one sample per run).
Currently, MIMIQ only implements the quantum trajectories method.
The basis for quantum trajectories is that a Kraus channel can be rewritten as:
where \(p_k = \mathrm{Tr}(E_k \rho E_k^\dagger)\) and \(\tilde{E}_k = E_k / \sqrt{p_k}\).
The parameters \(p_k\) can be interpreted as probabilities since they fulfill \(0 \leq p_k \leq 1\) and \(\sum_k p_k = 1\). In this way, the Kraus channel can be viewed as a linear combination of operations with different Kraus operators weighted by the probabilities \(p_k\).
Note that the probabilities \(p_k\) generally depend on the state, so they need to be computed at runtime. The exception is mixed-unitary channels, for which the probabilities are fixed (state-independent).
Building noise channels¶
You can create noise channels using one of the many functions available. Most noise channels take one or more parameters, and custom channels require passing the Kraus matrices and/or probabilities. Here are some examples of how to build noise channels:
>>> p = 0.1 # probability
>>> PauliX(p)
PauliX(0.1)
>>> p, gamma = 0.1, 0.2 # parameters
>>> GeneralizedAmplitudeDamping(p, gamma)
GeneralizedAmplitudeDamping((0.1, 0.2))
>>> ps = [0.8, 0.1, 0.1] # probabilities
>>> paulis = ["II", "XX", "YY"] # Pauli strings
>>> PauliNoise(ps, paulis)
PauliNoise((0.8, pauli"II"), (0.1, pauli"XX"), (0.1, pauli"YY"))
>>> from symengine import *
>>> ps = [0.9, 0.1] # probabilities
>>> unitaries = [Matrix([[1, 0], [0, 1]]), Matrix([[1, 0], [0, -1]])] # unitary matrices
>>> MixedUnitary(ps, unitaries)
MixedUnitary((0.9, "Custom([1 0; 0 1])"), (0.1, "Custom([1 0; 0 -1])"))
>>> kmatrices = [Matrix([[1, 0], [0, (0.9)**0.5]]), Matrix([[0, (0.1)**0.5], [0, 0]])] # Kraus matrices
>>> Kraus(kmatrices)
Kraus(Operator([[1, 0], [0, 0.948683298050514]]), Operator([[0, 0.316227766016838], [0, 0]]))
Check the documentation for each noise channel to understand the conditions that each parameter needs to fulfill for the noise channel to be valid.
In MIMIQ, the most important distinction between noise channels is whether they are mixed-unitary or general Kraus channels. You can use ismixedunitary() to check if a channel is mixed unitary:
>>> PauliX(0.1).ismixedunitary()
True
>>> AmplitudeDamping(0.1).ismixedunitary()
False
You can retrieve the Kraus matrices/operators used to define a given channel using krausmatrices() or krausoperators(). For example:
>>> ProjectiveNoise("Z").krausmatrices()
[[1.0, 0]
[0, 0]
, [0, 0]
[0, 1.0]
]
For mixed unitary channels, you can obtain the list of probabilities and the list of unitary gates/matrices separately using probabilities(), unitarymatrices(), or unitarygates(), respectively:
>>> PauliZ(0.1).unitarymatrices()
[[1.0, 0]
[0, 1.0]
, [1.0, 0]
[0, -1.0]
]
>>> Depolarizing1(0.1).unitarygates()
[I, X, Y, Z]
>>> PauliNoise([0.1, 0.9], ["II", "ZZ"]).probabilities()
[0.1, 0.9]
In MIMIQ, noise channels can be added at any point in a circuit to make any operation noisy. For noisy gates, one would normally add a noise channel after an ideal gate. To model measurement, preparation, and reset errors, noise channels can be added before and/or after the corresponding operation. More information can be found in the next section.
How to add noise¶
Adding noise one by one¶
The simplest and most flexible way to add noise to a circuit is by using the push() method, similar to how gates are added. Here’s an example of how to create a noisy 5-qubit GHZ circuit:
>>> c = Circuit()
>>> c.push(PauliX(0.1), [1, 2, 3, 4, 5]) # preparation/reset error since all qubits start in 0
6-qubit circuit with 5 instructions:
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
└── PauliX(0.1) @ q[5]
>>> c.push(GateH(), 1)
6-qubit circuit with 6 instructions:
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
└── H @ q[1]
>>> c.push(AmplitudeDamping(0.1), 1) # 1-qubit noise for GateH
6-qubit circuit with 7 instructions:
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
└── AmplitudeDamping(0.1) @ q[1]
>>> c.push(GateCX(), 1, [2, 3, 4, 5])
6-qubit circuit with 11 instructions:
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
├── AmplitudeDamping(0.1) @ q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[3]
├── CX @ q[1], q[4]
└── CX @ q[1], q[5]
>>> c.push(Depolarizing2(0.1), 1, [2, 3, 4, 5]) # 2-qubit noise for GateCX
6-qubit circuit with 15 instructions:
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
├── AmplitudeDamping(0.1) @ q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[3]
├── CX @ q[1], q[4]
├── CX @ q[1], q[5]
├── Depolarizing(0.1) @ q[1,2]
├── Depolarizing(0.1) @ q[1,3]
├── Depolarizing(0.1) @ q[1,4]
└── Depolarizing(0.1) @ q[1,5]
>>> c.push(PauliX(0.1), [1, 2, 3, 4, 5]) # measurement error. Note it's added before the measurement
6-qubit circuit with 20 instructions:
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
├── AmplitudeDamping(0.1) @ q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[3]
├── CX @ q[1], q[4]
├── CX @ q[1], q[5]
├── Depolarizing(0.1) @ q[1,2]
├── Depolarizing(0.1) @ q[1,3]
├── Depolarizing(0.1) @ q[1,4]
├── Depolarizing(0.1) @ q[1,5]
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
└── PauliX(0.1) @ q[5]
>>> c=c.push(Measure(), [1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> print(c)
6-qubit circuit with 25 instructions:
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
├── AmplitudeDamping(0.1) @ q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[3]
├── CX @ q[1], q[4]
├── CX @ q[1], q[5]
├── Depolarizing(0.1) @ q[1,2]
├── Depolarizing(0.1) @ q[1,3]
├── Depolarizing(0.1) @ q[1,4]
├── Depolarizing(0.1) @ q[1,5]
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── M @ q[1], c[1]
├── M @ q[2], c[2]
├── M @ q[3], c[3]
├── M @ q[4], c[4]
└── M @ q[5], c[5]
Note how bit-flip error (PauliX) is added at the beginning for state preparation/reset errors and right before measuring for measurement errors.
Adding noise to all gates of the same type¶
Usually, when adding noise to a circuit, the same type of noise is added to each instance of a given gate. Instead of adding noise channels one by one, you can use add_noise(). It takes several parameters:
Circuit().add_noise(gate, kraus, before=False, parallel=False)
This function adds the noise channel specified by kraus to every instance of the gate g in the circuit circuit. The optional parameter before determines whether to add the noise before or after the operation, and parallel determines whether to add the noise in parallel after/before a block of transversal gates.
Here’s the same noisy GHZ circuit, using add_noise():
>>> cnoise = c.add_noise(Reset(), PauliX(0.1), parallel=True)
>>> cnoise.add_noise(GateH(), AmplitudeDamping(0.1))
6-qubit, 6-bit circuit with 21 instructions:
├── Reset @ q[1]
├── Reset @ q[2]
├── Reset @ q[3]
├── Reset @ q[4]
├── Reset @ q[5]
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
├── AmplitudeDamping(0.1) @ q[1]
├── CX @ q[1], q[2]
├── CX @ q[1], q[3]
├── CX @ q[1], q[4]
├── CX @ q[1], q[5]
├── M @ q[1], c[1]
├── M @ q[2], c[2]
├── M @ q[3], c[3]
⋮ ⋮
└── M @ q[5], c[5]
>>> cnoise.add_noise(GateCX(), Depolarizing2(0.1), parallel=True)
6-qubit, 6-bit circuit with 25 instructions:
├── Reset @ q[1]
├── Reset @ q[2]
├── Reset @ q[3]
├── Reset @ q[4]
├── Reset @ q[5]
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
├── AmplitudeDamping(0.1) @ q[1]
├── CX @ q[1], q[2]
├── Depolarizing(0.1) @ q[1,2]
├── CX @ q[1], q[3]
├── Depolarizing(0.1) @ q[1,3]
├── CX @ q[1], q[4]
├── Depolarizing(0.1) @ q[1,4]
├── CX @ q[1], q[5]
⋮ ⋮
└── M @ q[5], c[5]
>>> cnoise.add_noise(Measure(), PauliX(0.1), before=True, parallel=True)
6-qubit, 6-bit circuit with 30 instructions:
├── Reset @ q[1]
├── Reset @ q[2]
├── Reset @ q[3]
├── Reset @ q[4]
├── Reset @ q[5]
├── PauliX(0.1) @ q[1]
├── PauliX(0.1) @ q[2]
├── PauliX(0.1) @ q[3]
├── PauliX(0.1) @ q[4]
├── PauliX(0.1) @ q[5]
├── H @ q[1]
├── AmplitudeDamping(0.1) @ q[1]
├── CX @ q[1], q[2]
├── Depolarizing(0.1) @ q[1,2]
├── CX @ q[1], q[3]
├── Depolarizing(0.1) @ q[1,3]
├── CX @ q[1], q[4]
├── Depolarizing(0.1) @ q[1,4]
├── CX @ q[1], q[5]
⋮ ⋮
└── M @ q[5], c[5]
Running a noisy circuit¶
Circuits with noise can be run using the submit() function,
just as with circuits without noise. Currently, noisy simulations are run using the quantum trajectories
method. In this case, when running a circuit with noise for n samples, the circuit will internally be
run once for each sample, with a different set of random Kraus operators selected based on the
corresponding probabilities.
When the noise channel is a mixed-unitary channel, the unitary operators to be applied can be selected
before applying the operations on the state. Use the sample_mixedunitaries()
function to generate samples of a circuit with mixed-unitary noise:
>>> from mimiqcircuits import *
>>> import random
>>> rng = random.Random(42)
>>> c = Circuit()
>>> c.push(Depolarizing1(0.5), [1, 2, 3, 4, 5])
6-qubit circuit with 5 instructions:
├── Depolarizing(0.5) @ q[1]
├── Depolarizing(0.5) @ q[2]
├── Depolarizing(0.5) @ q[3]
├── Depolarizing(0.5) @ q[4]
└── Depolarizing(0.5) @ q[5]
# Produce a circuit with either I, X, Y, or Z in place of each depolarizing channel
>>> c.sample_mixedunitaries(rng=rng, ids=True)
6-qubit circuit with 5 instructions:
├── X @ q[1]
├── I @ q[2]
├── I @ q[3]
├── I @ q[4]
└── Y @ q[5]
This function is called internally when executing a circuit, but it can also be used separately.
Noise Models¶
In addition to adding noise directly to the circuit, MIMIQ provides a Noise Model framework. A NoiseModel is a collection of “noise rules” that define how noise should be applied to a circuit. This allows you to define a noise profile once and apply it to multiple circuits.
Building a Noise Model¶
A noise model is essentially a container for rules. You can create an empty noise model and add rules to it.
model = NoiseModel(name="My Noise Model")
The recommended way to add rules is using the helper methods:
add_operation_noise()add_readout_noise()add_idle_noise()
Operation Noise¶
Use add_operation_noise to add noise to specific operations (gates, measurements, resets, etc.). You can specify the operation type (or a symbolic pattern for gates), the noise channel, and optionally specific qubits.
# Add noise to all Hadamard gates
model.add_operation_noise(GateH(), Depolarizing1(0.001))
# Add noise to CX gates only on qubits [1, 2]
model.add_operation_noise(GateCX(), Depolarizing2(0.01), qubits=[1, 2], exact=True)
# Add noise to any CX gate involving qubits in set {1, 2, 3}
model.add_operation_noise(GateCX(), Depolarizing2(0.005), qubits=[1, 2, 3], exact=False)
# Add noise before measurements
model.add_operation_noise(Measure(), PauliX(0.02), before=True)
# Add noise after resets
model.add_operation_noise(Reset(), Depolarizing1(0.01))
# Replace an operation with noise (e.g., replace a gate with a noise channel)
model.add_operation_noise(GateH(), AmplitudeDamping(0.001), replace=True)
Readout Noise¶
Use add_readout_noise to add noise to measurements.
# Global readout error
model.add_readout_noise(ReadoutErr(0.01, 0.02))
# Readout error only on qubit 1
model.add_readout_noise(ReadoutErr(0.05, 0.05), qubits=[1])
Idle Noise¶
Use add_idle_noise to add noise to idle qubits (during Delay operations).
# Constant idle noise
model.add_idle_noise(AmplitudeDamping(0.0001))
# Time-dependent idle noise (using symbolic variable t)
from symengine import Symbol
t = Symbol("t")
model.add_idle_noise(t, AmplitudeDamping(t / 1000.0))
Applying a Noise Model¶
Once you have defined a noise model, you can apply it to a circuit using apply_noise_model(). This will return a new circuit with the noise instructions inserted.
noisy_circuit = apply_noise_model(circuit, model)
Adding Rules Directly¶
While helper functions are convenient, you can also add rules directly to the model using add_rule(). This gives you full control over the rule parameters.
model.add_rule(GlobalReadoutNoise(ReadoutErr(0.01, 0.01)))
Available Rules¶
Here is a list of the available noise rules. Each rule matches a specific condition in the circuit.
Readout Noise
GlobalReadoutNoise: Applies noise to all measurements.SetQubitReadoutNoise: Applies noise to measurements on any qubit in a given set.ExactQubitReadoutNoise: Applies noise to measurements on a specific sequence of qubits (sensitive to order).
Operation Noise
OperationInstanceNoise: Applies noise to all instances of a specific operation type (e.g., allCXgates, all measurements, all resets).SetOperationInstanceQubitNoise: Applies noise to operations acting on qubits within a given set.ExactOperationInstanceQubitNoise: Applies noise to operations acting on a specific sequence of qubits.
Idle Noise
IdleNoise: Applies noise to idle qubits duringDelayinstructions.SetIdleQubitNoise: Applies idle noise only to specific qubits.
Custom Rules
CustomNoiseRule: User-defined rule with custom matching and noise generation logic. Has the highest priority by default.
# Custom rule with user-defined matching and generation functions
rule = CustomNoiseRule(
matcher=lambda inst: isinstance(inst.get_operation(), GateH),
generator=lambda inst: Instruction(AmplitudeDamping(0.01), tuple(inst.get_qubits())),
priority_val=0, # highest priority (default)
)
model.add_rule(rule)
Saving and Loading¶
Noise models can be saved to and loaded from disk using the protobuf format. This allows sharing models between Python and Julia.
# Save to a file
model.saveproto("noise_model.pb")
# Load from a file
loaded_model = NoiseModel.loadproto("noise_model.pb")
Reference¶
- class mimiqcircuits.NoiseModel(rules=<factory>, name='')[source]
Bases:
objectCollection of prioritized noise rules.
Rules are always kept sorted by
priority()so more specific rules win over generic fallbacks.Priority order (lower number = higher priority): -
CustomNoiseRule(defaultPRIORITY_USER_OVERRIDE) -ExactOperationInstanceQubitNoise(PRIORITY_EXACT_OPERATION) -ExactQubitReadoutNoise(PRIORITY_EXACT_READOUT) -SetOperationInstanceQubitNoise(PRIORITY_SET_OPERATION) -SetQubitReadoutNoise(PRIORITY_SET_READOUT) -OperationInstanceNoise(PRIORITY_GLOBAL_OPERATION) -GlobalReadoutNoise(PRIORITY_GLOBAL_READOUT) -SetIdleQubitNoise(PRIORITY_SET_IDLE) -IdleNoise(PRIORITY_IDLE)- Parameters:
rules (List[AbstractNoiseRule]) – Initial list of rules.
name (str) – Optional model name.
Example
>>> import mimiqcircuits as mc >>> from symengine import symbols, pi >>> theta = symbols("theta") >>> model = mc.NoiseModel( ... [ ... mc.OperationInstanceNoise(mc.GateRX(theta), mc.Depolarizing(1, theta / pi)), ... mc.ExactOperationInstanceQubitNoise(mc.GateCX(), [0, 1], mc.Depolarizing(2, 0.01)), ... mc.GlobalReadoutNoise(mc.ReadoutErr(0.01, 0.02)), ... mc.IdleNoise(mc.AmplitudeDamping(1e-4)), ... ], ... name="angle-dependent", ... )
- rules: List[AbstractNoiseRule]
- name: str = ''
- add_rule(rule)[source]
Add a rule and keep model rules sorted by priority.
- add_readout_noise(noise, *, qubits=None, exact=False)[source]
Add a readout-noise rule.
Behavior: -
qubits is None: addGlobalReadoutNoise. -qubitswithexact=False: addSetQubitReadoutNoise. -qubitswithexact=True: addExactQubitReadoutNoise.- Parameters:
noise (ReadoutErr) – Readout error channel.
qubits – Optional qubit targets.
exact – When
True, qubit order must match exactly.
- Returns:
self(for chaining).
Example
>>> import mimiqcircuits as mc >>> model = mc.NoiseModel() >>> model.add_readout_noise(mc.ReadoutErr(0.01, 0.02)) NoiseModel(rules=[GlobalReadoutNoise(noise=RErr(0.01,0.02))], name='') >>> model.add_readout_noise(mc.ReadoutErr(0.03, 0.04), qubits=[0, 2]) NoiseModel(rules=[SetQubitReadoutNoise(qubits=(0, 2), noise=RErr(0.03,0.04)), GlobalReadoutNoise(noise=RErr(0.01,0.02))], name='') >>> model.add_readout_noise(mc.ReadoutErr(0.05, 0.06), qubits=[2, 0], exact=True) NoiseModel(rules=[ExactQubitReadoutNoise(qubits=(2, 0), noise=RErr(0.05,0.06)), SetQubitReadoutNoise(qubits=(0, 2), noise=RErr(0.03,0.04)), GlobalReadoutNoise(noise=RErr(0.01,0.02))], name='')
- add_operation_noise(operation, noise, *, qubits=None, exact=False, before=False, replace=False)[source]
Add operation-instance noise.
operationcan be concrete (exact parameter match) or symbolic (match by operation type and substitute parameters intonoise).- Parameters:
operation – Operation pattern to match.
noise – Noise operation/channel to inject.
qubits – Optional qubit restriction.
exact – If
True, qubits must match the exact ordered tuple.before – If
True, insert noise before the matched instruction.replace – If
True, replace the matched instruction with noise.
- Returns:
self(for chaining).- Raises:
ValueError – If operation target is unsupported or arguments are invalid.
Examples
>>> import mimiqcircuits as mc >>> from symengine import symbols, pi >>> theta, alpha, beta = symbols("theta alpha beta") >>> model = mc.NoiseModel() >>> _ = model.add_operation_noise(mc.GateRX(pi / 2), mc.AmplitudeDamping(0.001)) >>> _ = model.add_operation_noise(mc.GateRX(theta), mc.Depolarizing(1, theta / pi)) >>> _ = model.add_operation_noise( ... mc.GateU(alpha, beta, 0), ... mc.Depolarizing(1, (alpha**2 + beta**2) / (2 * pi**2)), ... ) >>> _ = model.add_operation_noise( ... mc.GateRX(theta), ... mc.Depolarizing(1, theta / pi), ... qubits=[0, 1, 2], ... ) >>> _ = model.add_operation_noise( ... mc.GateRX(theta), ... mc.Depolarizing(1, theta / pi), ... qubits=[0], ... exact=True, ... ) >>> _ = model.add_operation_noise(mc.Measure(), mc.PauliX(0.02), before=True) >>> _ = model.add_operation_noise(mc.Reset(), mc.Depolarizing(1, 0.01)) >>> _ = model.add_operation_noise(mc.GateH(), mc.AmplitudeDamping(0.001), replace=True)
- add_idle_noise(noise, qubits=None)[source]
Add idle-noise rule(s) for
Delayinstructions.noisecan be a constant operation or a relation tuple(time_symbol, target_operation_expr).- Parameters:
noise – Idle noise operation or symbolic relation.
qubits – Optional qubit subset where idle noise is allowed.
- Returns:
self(for chaining).
Example
>>> import mimiqcircuits as mc >>> from symengine import symbols >>> t = symbols("t") >>> model = mc.NoiseModel() >>> model.add_idle_noise(mc.AmplitudeDamping(1e-4)) NoiseModel(rules=[IdleNoise(relation=AmplitudeDamping(0.0001))], name='') >>> model.add_idle_noise((t, mc.AmplitudeDamping(t / 1000)), qubits=[0, 1, 2]) NoiseModel(rules=[SetIdleQubitNoise(relation=(t, AmplitudeDamping((1/1000)*t)), qubits=frozenset({0, 1, 2})), IdleNoise(relation=AmplitudeDamping(0.0001))], name='')
- apply_noise_model(circuit)[source]
Apply this noise model and return a new noisy circuit.
This is a convenience wrapper around module-level
apply_noise_model.
- describe()[source]
- saveproto(file)[source]
- static loadproto(file)[source]
- __init__(rules=<factory>, name='')
- mimiqcircuits.apply_noise_model(circuit, model)[source]
Apply a noise model to a circuit and return a new circuit.
Rules are evaluated in priority order. For each instruction, only the first matching rule is applied.
Wrapper operations are traversed recursively:
Block,IfStatement,Parallel,Repeat, andGateCall.- Parameters:
circuit (Circuit) – Input circuit.
model (NoiseModel) – Noise model to apply.
- Returns:
A new circuit with injected noise.
Examples
>>> import mimiqcircuits as mc >>> from symengine import symbols, pi >>> theta = symbols("theta") >>> c = mc.Circuit() >>> c.push(mc.GateRX(0.4), 0) 1-qubit circuit with 1 instruction: └── RX(0.4) @ q[0] >>> c.push(mc.GateRX(0.8), 1) 2-qubit circuit with 2 instructions: ├── RX(0.4) @ q[0] └── RX(0.8) @ q[1] >>> c.push(mc.Measure(), 0, 0) 2-qubit, 1-bit circuit with 3 instructions: ├── RX(0.4) @ q[0] ├── RX(0.8) @ q[1] └── M @ q[0], c[0] >>> c.push(mc.Measure(), 1, 1) 2-qubit, 2-bit circuit with 4 instructions: ├── RX(0.4) @ q[0] ├── RX(0.8) @ q[1] ├── M @ q[0], c[0] └── M @ q[1], c[1] >>> model = mc.NoiseModel([mc.OperationInstanceNoise(mc.GateRX(theta), mc.Depolarizing(1, theta / pi)), ... mc.GlobalReadoutNoise(mc.ReadoutErr(0.01, 0.02)),]) >>> noisy = mc.apply_noise_model(c, model)
- Recursive wrapper examples:
>>> model = mc.NoiseModel([mc.OperationInstanceNoise(mc.GateH(), mc.AmplitudeDamping(0.01))]) >>> inner = mc.Circuit().push(mc.GateH(), 0) >>> c_block = mc.Circuit().push(mc.Block(inner), 0) >>> noisy_block= mc.apply_noise_model(c_block, model) >>> noisy_block.decompose() 1-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0] >>> decl = mc.GateDecl("local_h", (), inner) >>> c_call = mc.Circuit().push(mc.GateCall(decl, ()), 0) >>> noisy_call = mc.apply_noise_model(c_call, model) >>> noisy_call.decompose() 1-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0] >>> c_parallel = mc.Circuit().push(mc.Parallel(2, mc.GateH()), 0, 1) >>> noisy_parallel = mc.apply_noise_model(c_parallel, model) >>> noisy_parallel.decompose() 2-qubit circuit with 4 instructions: ├── H @ q[0] ├── AmplitudeDamping(0.01) @ q[0] ├── H @ q[1] └── AmplitudeDamping(0.01) @ q[1] >>> c_repeat = mc.Circuit().push(mc.Repeat(2, mc.GateH()), 0) >>> noisy_repeat = mc.apply_noise_model(c_repeat, model) >>> noisy_repeat.decompose() 1-qubit circuit with 4 instructions: ├── H @ q[0] ├── AmplitudeDamping(0.01) @ q[0] ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0] >>> c_if = mc.Circuit().push(mc.IfStatement(mc.GateH(), mc.BitString("1")), 0, 0) >>> noisy_if = mc.apply_noise_model(c_if, model) >>> noisy_if.decompose() 1-qubit, 1-bit circuit with 2 instructions: ├── IF(c==1) H @ q[0], condition[0] └── IF(c==1) AmplitudeDamping(0.01) @ q[0], condition[0]
>>> decl_inner = mc.GateDecl("inner_h", (), inner) >>> middle = mc.Circuit().push(mc.GateCall(decl_inner, ()), 0) >>> decl_outer = mc.GateDecl("outer_call", (), middle) >>> c_nested = mc.Circuit().push(mc.GateCall(decl_outer, ()), 0) >>> noisy_nested = mc.apply_noise_model(c_nested, model) >>> noisy_nested.decompose().decompose() 1-qubit circuit with 2 instructions: ├── H @ q[0] └── AmplitudeDamping(0.01) @ q[0]
- Recursive wrapper examples deeply nested:
>>> c_nested2 = mc.Circuit().push(mc.GateCall(decl_outer, ()), 0).push(mc.GateCall(decl_outer, ()), 1) >>> c_nested2.decompose() 2-qubit circuit with 2 instructions: ├── inner_h() @ q[0] └── inner_h() @ q[1] >>> noisy_nested2 = mc.apply_noise_model(c_nested2, model) >>> noisy_nested2.decompose().decompose() 2-qubit circuit with 4 instructions: ├── H @ q[0] ├── AmplitudeDamping(0.01) @ q[0] ├── H @ q[1] └── AmplitudeDamping(0.01) @ q[1]
- class mimiqcircuits.Depolarizing1(p)[source]
Bases:
krauschannel
- class mimiqcircuits.Depolarizing2(p)[source]
Bases:
krauschannel
- class mimiqcircuits.AmplitudeDamping(gamma)[source]
Bases:
krauschannelOne-qubit amplitude damping noise channel.
The amplitude damping channel is defined by the Kraus operators .. seealso::
GeneralizedAmplitudeDamping()Kraus Matrices representation:
\[\begin{split}\operatorname E_1 = \begin{pmatrix} 1 & 0 \\ 0 & \sqrt{1-\gamma} \end{pmatrix} ,\quad \operatorname E_2 = \begin{pmatrix} 0 & \sqrt{\gamma} \\ 0 & 0 \end{pmatrix},\end{split}\]- Parameters:
gamma –
gamma in [0,1].
Examples
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(AmplitudeDamping(0.1), 0) 1-qubit circuit with 1 instruction: └── AmplitudeDamping(0.1) @ q[0]
- __init__(gamma)[source]
- evaluate(d={})[source]
- krausoperators()[source]
Returns the Kraus operators associated with the given Kraus channel.
This should be implemented for each specific channel.
- Returns:
A list of matrices representing the Kraus operators.
- Return type:
- krausmatrices()[source]
Returns the Kraus matrices associated with the given Kraus channel.
A mixed unitary channel is written as:
\[\mathcal{E}(\rho) = \sum_k p_k U_k \rho U_k^\dagger,\]where \(U_k\) are the unitary matrices returned by this function.
If the Kraus channel is parametric, the matrix elements are wrapped in a symengine or sympy object.
- Returns:
A list of symengine matrices representing the Kraus operators.
- Return type:
- iswrapper()[source]
- property opname
- property parnames
- class mimiqcircuits.PauliNoise(p, paulistr)[source]
Bases:
krauschannelN-qubit Pauli noise channel specified by a list of probabilities and Pauli gates.
A Pauli channel is defined by:
\[\mathcal{E}(\rho) = \sum_k p_k P_k \rho P_k,\]where \(0 \leq p_k \leq 1\) and \(P_k\) are Pauli string operators, defined as tensor products of one-qubit Pauli operators. The probabilities must fulfill \(\sum_k p_k = 1\).
This channel is a mixed unitary channel (see
ismixedunitary()).See also
Depolarizing,PauliX,PauliY,PauliZ, which are special cases of PauliNoise.- Parameters:
The lengths of p and paulistrings must be the same.
Examples
PauliNoise channels can be defined for any number of qubits, and for any number of Pauli strings:
>>> from mimiqcircuits import * >>> c = Circuit() >>> c.push(PauliNoise([0.8, 0.1, 0.1], ["I", "X", "Y"]), 1) 2-qubit circuit with 1 instruction: └── PauliNoise((0.8, pauli"I"), (0.1, pauli"X"), (0.1, pauli"Y")) @ q[1]
>>> c.push(PauliNoise([0.9, 0.1], ["XY", "II"]), 1, 2) 3-qubit circuit with 2 instructions: ├── PauliNoise((0.8, pauli"I"), (0.1, pauli"X"), (0.1, pauli"Y")) @ q[1] └── PauliNoise((0.9, pauli"XY"), (0.1, pauli"II")) @ q[1]
>>> c.push(PauliNoise([0.5, 0.2, 0.2, 0.1], ["IXIX", "XYXY", "ZZZZ", "IXYZ"]), 1, 2, 3, 4) 5-qubit circuit with 3 instructions: ├── PauliNoise((0.8, pauli"I"), (0.1, pauli"X"), (0.1, pauli"Y")) @ q[1] ├── PauliNoise((0.9, pauli"XY"), (0.1, pauli"II")) @ q[1] └── PauliNoise((0.5, pauli"IXIX"), (0.2, pauli"XYXY"), (0.2, pauli"ZZZZ"), (0.1, pauli"IXYZ")) @ q[1]
- __init__(p, paulistr)[source]
- property num_qubits
- property parnames
- evaluate(d={})[source]
- krausmatrices()[source]
Returns the Kraus matrices associated with the given Kraus channel.
A mixed unitary channel is written as:
\[\mathcal{E}(\rho) = \sum_k p_k U_k \rho U_k^\dagger,\]where \(U_k\) are the unitary matrices returned by this function.
If the Kraus channel is parametric, the matrix elements are wrapped in a symengine or sympy object.
- Returns:
A list of symengine matrices representing the Kraus operators.
- Return type:
- krausoperators()[source]
Returns the Kraus operators associated with the given Kraus channel.
This should be implemented for each specific channel.
- Returns:
A list of matrices representing the Kraus operators.
- Return type:
- probabilities()[source]
Returns the probabilities for each Kraus operator in a mixed unitary channel.
A mixed unitary channel is written as:
\[\mathcal{E}(\rho) = \sum_k p_k U_k \rho U_k^\dagger,\]where \(p_k\) are the probabilities.
This method is valid only for mixed unitary channels.
- Returns:
A list of probabilities for each Kraus operator.
- Return type:
- unitarymatrices()[source]
Unitary matrices associated with the given mixed unitary Kraus channel.
A mixed unitary channel is written as:
\[\mathcal{E}(\rho) = \sum_k p_k U_k \rho U_k^\dagger,\]where \(U_k\) are the unitary matrices.
An error is raised if the channel is not mixed unitary (i.e., ismixedunitary(self)==False).
Note
If the Kraus channel is parametric, the matrix elements are wrapped in a symbolic object (e.g., from sympy or symengine). To manipulate expressions, use the appropriate symbolic manipulation libraries.
See also
ismixedunitary()probabilities()krausmatrices()Examples
>>> from mimiqcircuits import * >>> PauliX(0.2).unitarymatrices() [[1.0, 0] [0, 1.0] , [0, 1.0] [1.0, 0] ]
- unitarygates()[source]
Returns the unitary gates associated with the given mixed unitary Kraus channel.
A mixed unitary channel is written as:
\[\sum_k p_k U_k \rho U_k^\dagger,\]where \(U_k\) are the unitary operators.
This method is valid only for mixed unitary channels.
- classmethod ismixedunitary()[source]
Determine whether the quantum operation is a mixed unitary channel.
A channel is considered mixed unitary if all the Kraus operators \(E_k\) are proportional to a unitary matrix \(U_k\), i.e.,
\[\mathcal{E}(\rho) = \sum_k p_k U_k \rho U_k^\dagger,\]with some probabilities \(0 \leq p_k \leq 1\) that add up to 1, and \(U_k^\dagger U_k = I\).
- Parameters:
krauschannel – The Kraus channel to check.
- Returns:
True if the channel is a mixed unitary channel, False otherwise.
- Return type:
Examples
>>> from mimiqcircuits import * >>> PauliX(0.1).ismixedunitary() True
>>> AmplitudeDamping(0.1).ismixedunitary() False
- iswrapper()[source]
- unwrappedkrausmatrices()[source]
Returns the unitary Kraus matrices associated to the mixed unitary Kraus channel without symbolic wrapper.
See also
unitarymatrices()Example
>>> from mimiqcircuits import * >>> op = PauliX(0.2) >>> op.unwrappedkrausmatrices() [[0.894427190999916, 0] [0, 0.894427190999916] , [0, 0.447213595499958] [0.447213595499958, 0] ]
- class mimiqcircuits.ReadoutErr(p0, p1=None)[source]
Bases:
OperationRepresents a classical readout error applied immediately after a measurement.
The readout error is modeled by a 2×2 confusion matrix:
\[\begin{split}\begin{pmatrix} P(\text{report } 0 | \text{true } 0) & P(\text{report } 1 | \text{true } 0) = p_0 \\ P(\text{report } 0 | \text{true } 1) = p_1 & P(\text{report } 1 | \text{true } 1) \end{pmatrix}\end{split}\]Can be initialized either from the error probabilities
p0,p1or directly from a 2×2 confusion matrix. Accepts NumPy, SymPy, or SymEngine matrices.Examples
>>> from mimiqcircuits import * >>> ReadoutErr(0.02, 0.03) RErr(0.02,0.03)
>>> import numpy as np >>> ReadoutErr(np.array([[0.98, 0.02], ... [0.03, 0.97]])) RErr(0.02,0.03)
>>> c = Circuit() >>> c.push(ReadoutErr(0.01, 0.02),0) 1-bit circuit with 1 instruction: └── RErr(0.01, 0.02) @ c[0]
- __init__(p0, p1=None)[source]
- evaluate(d={})[source]
Evaluate symbolic parameters numerically. Returns a new ReadoutErr instance with evaluated parameters.
- matrix()[source]
Return the confusion matrix as a symengine.Matrix.
- property opname
- property parnames
- iswrapper()[source]