Noisy simulations on MIMIQ
Here we explain how to run noisy simulations that mimic the behavior of real quantum computers on MIMIQ. In this section you'll find:
Summary of noise functionality
Custom noise channels:
Specialized noise channels:
DepolarizingDepolarizing1Depolarizing2PauliNoisePauliXPauliYPauliZAmplitudeDampingGeneralizedAmplitudeDampingPhaseAmplitudeDampingThermalNoiseProjectiveNoiseProjectiveNoiseX,ProjectiveNoiseY,ProjectiveNoiseZ(whatProjectiveNoise("X"),ProjectiveNoise("Y")andProjectiveNoise("Z")return)
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 (MimiqCircuitsBase.GATES).
Noise channels come with the following methods:
krausmatricesandkrausoperatorsunitarymatricesandunitarygates(only for mixed-unitary)probabilities(only for mixed-unitary)ismixedunitary
To add noise channels to a circuit you can use:
push!(like gates)add_noiseoradd_noise!(add noise to every instance of a gate)
To generate one sample of a circuit with mixed unitaries use:
See below for further information. You can also run ? followed by the given function in the command line (e.g. ?Kraus), or using @doc in Jupyter (e.g. @doc Kraus).
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$, $\mathrm{Tr} \rho = 1$. A quantum operation such as noise can then be described using the Kraus operator representation as
\[ \mathcal{E}(\rho) = \sum_k E_k \rho E_k^\dagger.\]
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 the fulfill the completeness relation $\sum_k E_k^\dagger E_k = I$. 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 ($p_k = |\alpha_k|^2$)
\[ \mathcal{E}(\rho) = \sum_k p_k U_k \rho U_k^\dagger.\]
Such operations are easier to implement as we'll see below.
Remarks:
- Unitary gates $U$ just 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$ that happens in the circuit as a noisy quantum operation $\mathcal{E}_O$. The full noisy operation can in principle be described using Kraus operators, but usually it is decomposed as $\tilde{O} = \mathcal{E}_2 \circ O \circ \mathcal{E}_1$, where $\mathcal{E}_1$ and $\mathcal{E}_2$ are noise channels. In the case of gates we usually only consider a noise channel after the gate. Note that one common assumption in this type of noise modeling is that the noise channels of different gates are independent from each other.
For more details on noise see for example Nielsen and Chuang, Quantum Computation and Quantum Information, Chapter 8.
Evolution with noise
There are two common ways to evolve the state of the system when acting with Kraus channels as defined above:
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
\[ \mathcal{E}(\rho) = \sum_k p_k \tilde{E}_k \rho \tilde{E}_k^\dagger,\]
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), see above.
Building noise channels
You can create noise channels using one of the many functions available, see summary. Most of the noise channels take one or more parameters, and the custom channels require passing the Kraus matrices and/or probabilities. Here are some examples of how to build noise channels:
using MimiqCircuits
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"pauli"II""), (0.1, pauli"pauli"XX""), (0.1, pauli"pauli"YY""))ps = [0.9, 0.1] # probabilities
unitaries = [[1 0; 0 1], [1 0; 0 -1]] # unitary matrices
MixedUnitary(ps, unitaries)MixedUnitary((0.9, Custom([1.0 0.0; 0.0 1.0])), (0.1, Custom([1.0 0.0; 0.0 -1.0])))kmatrices = [[1 0; 0 sqrt(0.9)], [0 sqrt(0.1); 0 0]] # Kraus matrices
Kraus(kmatrices)Kraus(Operator([1.0 0.0; 0.0 0.948683…]), Operator([0.0 0.316228…; 0.0 0.0]))Custom Kraus channels also support LossyOperator branches, which makes them loss-aware Kraus channels. For more information, see the loss page.
Check the documentation of each noise channel to understand the conditions that each of the parameters needs to fulfill for the noise channel to be valid.
In MIMIQ the most important distinction of noise channels is between mixed unitary and general Kraus channels (see mathematical section for definitions). You can use ismixedunitary to check if a channel is mixed unitary or not like this:
ismixedunitary(PauliX(0.1))trueismixedunitary(AmplitudeDamping(0.1))falseIn both cases you can get the Kraus matrices/operators used to define the given channel by using krausmatrices or krausoperators. For example:
krausmatrices(ProjectiveNoise("Z"))2-element Vector{Matrix{Int64}}:
[1 0; 0 0]
[0 0; 0 1]In the case of mixed unitary channels, you can separately obtain the list of probabilities and the list of unitary gates/matrices using probabilities and unitarymatrices or unitarygates, respectively.
unitarymatrices(PauliZ(0.1))2-element Vector{Matrix{Float64}}:
[1.0 -0.0; 0.0 1.0]
[1.0 0.0; 0.0 -1.0]unitarygates(Depolarizing1(0.1))4-element Vector{AbstractGate{1}}:
ID
X
Y
Zprobabilities(PauliNoise([0.1,0.9],["II","ZZ"]))1×2 Matrix{Float64}:
0.1 0.9In MIMIQ, noise channels can be added at any point in the circuit in order 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 one can simply add noise channels before and/or after the corresponding operation. More information 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 push!(@ref), the same way that we add gates. Here's an example of how to create a noisy 5-qubit GHZ circuit:
c = Circuit()
push!(c, PauliX(0.1), 1:5) # preparation/reset error since all qubits start in 0
push!(c, GateH(), 1)
push!(c, AmplitudeDamping(0.1), 1) # 1-qubit noise for GateH
push!(c, GateCX(), 1, 2:5)
push!(c, Depolarizing2(0.1), 1, 2:5) # 2-qubit noise for GateCX
push!(c, PauliX(0.1), 1:5) # measurement error. Note it's added before the measurement
push!(c, Measure(), 1:5, 1:5)5-qubit, 5-bit 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]
⋮ ⋮
├── 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 we added bit-flip error (PauliX) at beginning for state preparation/reset errors and right before measuring for measurement errors.
Adding noise to all gates of same type
Usually, when we add noise to a circuit we want to add the same type of noise to each instance of a given gate. For this purpose, instead of adding noise channels one by one you can use add_noise or add_noise! (same but in-place). It takes several parameters:
add_noise(c, g, kraus; before=false, parallel=false)
This function will add the noise channel specified by kraus to every instance of gate g in the circuit c. The optional parameter before (default=false) determines whether to add the noise before or after the operation, and the parameter parallel (default=false) determines whether to add the noise in parallel after/before a block of transversal gates.
Here is how to construct the same example of a noisy GHZ circuit as before but with add_noise:
c = Circuit()
push!(c, Reset(), 1:5)
push!(c, GateH(), 1)
push!(c, GateCX(), 1, 2:5)
push!(c, Measure(), 1:5, 1:5)
cnoise = add_noise(c, Reset(), PauliX(0.1); parallel=true)
cnoise = add_noise(cnoise, GateH(), AmplitudeDamping(0.1))
cnoise = add_noise(cnoise, GateCX(), Depolarizing2(0.1); parallel=true)
cnoise = add_noise(cnoise, Measure(), PauliX(0.1); before=true, parallel=true)5-qubit, 5-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[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 that we added a trivial Reset operation at the very beginning just to be able to add the state preparation error with the add_noise functionality. The qubits already start at 0 anyway.
The add_noise function becomes particularly useful in big circuits with lots of repetitions of gates. For further details check the API documentation of add_noise and add_noise!.
Running a noisy circuit
Circuits with noise can be run with the same submit function as used for circuits without noise, see simulation and cloud execution pages. Recall that currently noisy simulations will be run using quantum trajectories. In this case, when running a circuit with noise for n samples the circuit will internally be run once for every sample. In every run, a different set of random Kraus operators will be 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 starting to apply operations on the state. We provide the function sample_mixedunitaries to generate samples of a circuit with mixed unitary noise as follows:
using Random
rng = MersenneTwister(42)
c = Circuit()
push!(c, Depolarizing1(0.5), 1:5)
# Produce a circuit with either I, X, Y, or Z in place of each depolarizing channel
csampled = sample_mixedunitaries(c; rng=rng, ids=true)5-qubit circuit with 5 instructions:
├── Y @ q[1]
├── ID @ q[2]
├── ID @ q[3]
├── ID @ q[4]
└── ID @ q[5]This function is internally called when executing a circuit, but can also be used outside of execution.
Noise Models
Adding noise instruction by instruction ties the noise to one circuit. A NoiseModel describes the noise of a device once, as a list of rules, and can then be applied to any number of circuits. Each rule says which instructions it matches (an operation, possibly restricted to some qubits, a measurement, an idle period) and which noise to insert.
Building a Noise Model
Start from an empty model, optionally with a name, and add rules with the helper functions:
add_operation_noise!for noise attached to gates, measurements and resets,add_readout_noise!for classical readout errors on measurements,add_idle_noise!for noise on qubits that wait during aDelay.
Each helper picks the right rule type for its arguments and returns the model.
model = NoiseModel(name="toy device")NoiseModel(AbstractNoiseRule[], "toy device")Operation noise
add_operation_noise! takes an operation and the noise to insert next to every matching instruction. The noise must act on as many qubits as the operation.
# Every Hadamard gate is followed by a depolarizing channel.
add_operation_noise!(model, GateH(), Depolarizing1(0.001))
# CX gates get stronger noise when the control is qubit 1 and the target
# qubit 2, in this order (`exact=true`).
add_operation_noise!(model, GateCX(), Depolarizing2(0.01); qubits=[1, 2], exact=true)
# Any other CX gate whose qubits all lie in {1, 2, 3} gets weaker noise.
add_operation_noise!(model, GateCX(), Depolarizing2(0.005); qubits=[1, 2, 3])NoiseModel(AbstractNoiseRule[ExactOperationInstanceQubitNoise(GateCX(), [1, 2], Depolarizing(2, 0.01), false, false), SetOperationInstanceQubitNoise(GateCX(), Set([3, 2, 1]), Depolarizing(2, 0.005), false, false), OperationInstanceNoise(GateH(), Depolarizing(1, 0.001), false, false)], "toy device")A concrete operation such as GateRX(π/2) matches only that exact operation. An operation with symbolic parameters matches every operation of the same type, and the parameters of the matched gate are substituted into the noise. This gives noise that depends on the rotation angle:
@variables θ
angle_model = NoiseModel()
add_operation_noise!(angle_model, GateRX(θ), Depolarizing1(θ / π))
c = push!(Circuit(), GateRX(0.5), 1)
apply_noise_model(c, angle_model)1-qubit circuit with 2 instructions:
├── RX(0.5) @ q[1]
└── Depolarizing(1,0.159155…) @ q[1]The keyword before=true inserts the noise before the operation instead of after it, and replace=true substitutes the operation with the noise. The two cannot be combined. The operation can be a gate, a measurement, a reset, or a Block, Repeat, IfStatement or WhileStatement; other operations, such as Barrier, raise an ArgumentError.
Readout noise
add_readout_noise! attaches a ReadoutErr to the classical bits written by a measurement. It matches every measurement operation (Measure, MeasureReset, …).
# Readout error on every measurement.
add_readout_noise!(model, ReadoutErr(0.01, 0.02))
# A worse readout error when measuring qubit 1.
add_readout_noise!(model, ReadoutErr(0.05, 0.05); qubits=[1])NoiseModel(AbstractNoiseRule[ExactOperationInstanceQubitNoise(GateCX(), [1, 2], Depolarizing(2, 0.01), false, false), SetOperationInstanceQubitNoise(GateCX(), Set([3, 2, 1]), Depolarizing(2, 0.005), false, false), SetQubitReadoutNoise(Set([1]), ReadoutErr(0.05, 0.05)), OperationInstanceNoise(GateH(), Depolarizing(1, 0.001), false, false), GlobalReadoutNoise(ReadoutErr(0.01, 0.02))], "toy device")With qubits the rule matches measurements whose qubits all lie in the given set; add exact=true to match only that exact sequence of qubits.
Idle noise
add_idle_noise! replaces every Delay instruction with a noise channel on the same qubits. The noise can be constant, or a relation t => channel(t) where t is a symbolic variable that takes the duration of the delay:
@variables t
# Amplitude damping that grows with the idle time.
add_idle_noise!(model, t => AmplitudeDamping(t / 1000))NoiseModel(AbstractNoiseRule[ExactOperationInstanceQubitNoise(GateCX(), [1, 2], Depolarizing(2, 0.01), false, false), SetOperationInstanceQubitNoise(GateCX(), Set([3, 2, 1]), Depolarizing(2, 0.005), false, false), SetQubitReadoutNoise(Set([1]), ReadoutErr(0.05, 0.05)), OperationInstanceNoise(GateH(), Depolarizing(1, 0.001), false, false), GlobalReadoutNoise(ReadoutErr(0.01, 0.02)), IdleNoise(t => AmplitudeDamping(t / 1000))], "toy device")Pass qubits to restrict idle noise to some qubits.
Idle noise only acts where the circuit has explicit Delay instructions; it does not detect qubits that are idle between gates.
Inspecting a Noise Model
describe prints the rules in the order they are tried, with their priorities:
describe(model)NoiseModel: toy device
==================================================
Rule 1 (priority 40): ExactOperationInstanceQubitNoise
→ Applies Depolarizing(2, 0.01) to GateCX() on qubits [1, 2] (exact order)
Rule 2 (priority 60): SetOperationInstanceQubitNoise
→ Applies Depolarizing(2, 0.005) to GateCX() on qubits in [1, 2, 3]
Rule 3 (priority 70): SetQubitReadoutNoise
→ Applies ReadoutErr(0.05, 0.05) to measurements on qubits in [1]
Rule 4 (priority 80): OperationInstanceNoise
→ Applies Depolarizing(1, 0.001) to GateH() instances
Rule 5 (priority 90): GlobalReadoutNoise
→ Applies ReadoutErr(0.01, 0.02) to all measurements
Rule 6 (priority 200): IdleNoise
→ Applies AmplitudeDamping(t / 1000) to all idle qubits using relation t => ...Applying a Noise Model
apply_noise_model returns a new circuit with the noise inserted, and apply_noise_model! modifies the circuit in place.
c = Circuit()
push!(c, GateH(), 1:3)
push!(c, GateCX(), 1, 2) # exact rule on (1, 2)
push!(c, GateCX(), 2, 3) # set rule on {1, 2, 3}
push!(c, GateCX(), 3, 4) # no rule matches: qubit 4 is outside the set
push!(c, Delay(50), 4) # idle noise with t = 50
push!(c, Measure(), 1:2, 1:2)
noisy = apply_noise_model(c, model)4-qubit, 2-bit circuit with 16 instructions:
├── H @ q[1]
├── Depolarizing(1,0.001) @ q[1]
├── H @ q[2]
├── Depolarizing(1,0.001) @ q[2]
├── H @ q[3]
├── Depolarizing(1,0.001) @ q[3]
├── CX @ q[1], q[2]
├── Depolarizing(2,0.01) @ q[1:2]
├── CX @ q[2], q[3]
├── Depolarizing(2,0.005) @ q[2:3]
├── CX @ q[3], q[4]
├── AmplitudeDamping(1//20) @ q[4]
├── M @ q[1], c[1]
├── RErr(0.05, 0.05) @ c[1]
├── M @ q[2], c[2]
└── RErr(0.01, 0.02) @ c[2]The rules also reach inside Block, GateCall, Parallel, Repeat, IfStatement and WhileStatement operations when no rule matches the wrapper itself.
Instead of applying the model yourself, you can pass it to submit as third argument, submit(conn, circuit, model), and it is applied to each circuit before upload.
Rule priority
Each instruction receives the noise of at most one rule. The rules of a model are kept sorted by priority, and for every instruction they are tried in that order; the first rule that matches is applied and the others are skipped. A lower number means a higher priority, so more specific rules win over general ones:
| Rule | Priority |
|---|---|
CustomNoiseRule | priority_val keyword, default 0 |
ExactOperationInstanceQubitNoise | 40 |
ExactQubitReadoutNoise | 50 |
SetOperationInstanceQubitNoise | 60 |
SetQubitReadoutNoise | 70 |
OperationInstanceNoise | 80 |
GlobalReadoutNoise | 90 |
SetIdleQubitNoise | 190 |
IdleNoise | 200 |
Rules with the same priority keep the order in which they were added.
In the example above, the CX on qubits (1, 2) matches both CX rules and only gets the exact one, and the measurement of qubit 1 gets the per-qubit readout error instead of the global one.
Because measurements are matched by both operation rules and readout rules, operation noise on Measure takes precedence over global readout noise:
both = NoiseModel()
add_operation_noise!(both, Measure(), PauliX(0.02); before=true)
add_readout_noise!(both, ReadoutErr(0.01, 0.02))
# Only the PauliX channel is inserted: the readout rule is never reached.
apply_noise_model(push!(Circuit(), Measure(), 1, 1), both)1-qubit, 1-bit circuit with 2 instructions:
├── PauliX(0.02) @ q[1]
└── M @ q[1], c[1]To get both effects on the same measurement, use a single CustomNoiseRule or add one of the two directly to the circuit.
Adding Rules Directly
The helper functions build rule objects for you. You can also construct a rule and add it with add_rule!, or pass a vector of rules to the NoiseModel constructor. add_rule! keeps the rules sorted by priority.
direct = NoiseModel([
OperationInstanceNoise(GateH(), AmplitudeDamping(0.01)),
GlobalReadoutNoise(ReadoutErr(0.01, 0.01)),
])
add_rule!(direct, SetIdleQubitNoise(AmplitudeDamping(0.001), [1, 2]))NoiseModel(AbstractNoiseRule[OperationInstanceNoise(GateH(), AmplitudeDamping(0.01), false, false), GlobalReadoutNoise(ReadoutErr(0.01, 0.01)), SetIdleQubitNoise(AmplitudeDamping(0.001), Set([2, 1]))], "")The available rules are:
| Rule | Matches | Helper |
|---|---|---|
OperationInstanceNoise | every instance of an operation | add_operation_noise! |
SetOperationInstanceQubitNoise | an operation whose qubits all lie in a set | add_operation_noise!(...; qubits) |
ExactOperationInstanceQubitNoise | an operation on an exact sequence of qubits | add_operation_noise!(...; qubits, exact=true) |
GlobalReadoutNoise | every measurement | add_readout_noise! |
SetQubitReadoutNoise | a measurement whose qubits all lie in a set | add_readout_noise!(...; qubits) |
ExactQubitReadoutNoise | a measurement on an exact sequence of qubits | add_readout_noise!(...; qubits, exact=true) |
IdleNoise | every Delay | add_idle_noise! |
SetIdleQubitNoise | a Delay whose qubits all lie in a set | add_idle_noise!(...; qubits) |
CustomNoiseRule | whatever its matcher function accepts | none |
The operation rules also accept a compact operation => noise form, e.g. OperationInstanceNoise(GateRX(θ) => Depolarizing1(θ / π)).
Custom rules
A CustomNoiseRule takes two functions: a matcher that receives an Instruction and returns whether the rule applies, and a generator that receives the same instruction and returns the noise instruction to insert. It accepts before and replace like the operation rules, and its priority is set with priority_val (default 0, tried before every built-in rule).
# Depolarizing noise on every two-qubit gate, whatever its type.
two_qubit = CustomNoiseRule(
inst -> getoperation(inst) isa AbstractGate && numqubits(getoperation(inst)) == 2,
inst -> Instruction(Depolarizing2(0.02), getqubits(inst)...),
)
custom = NoiseModel(two_qubit)
c = Circuit()
push!(c, GateCX(), 1, 2)
push!(c, GateCZ(), 2, 3)
apply_noise_model(c, custom)3-qubit circuit with 4 instructions:
├── CX @ q[1], q[2]
├── Depolarizing(2,0.02) @ q[1:2]
├── CZ @ q[2], q[3]
└── Depolarizing(2,0.02) @ q[2:3]Custom rules hold Julia functions, so they cannot be saved with saveproto.
Saving and Loading
Noise models can be saved to and loaded from disk in the protobuf format with saveproto and loadproto.
fname = joinpath(mktempdir(), "noise_model.pb")
saveproto(fname, model)
loaded = loadproto(fname, NoiseModel)
describe(loaded)NoiseModel: toy device
==================================================
Rule 1 (priority 40): ExactOperationInstanceQubitNoise
→ Applies Depolarizing(2, 0.01) to GateCX() on qubits [1, 2] (exact order)
Rule 2 (priority 60): SetOperationInstanceQubitNoise
→ Applies Depolarizing(2, 0.005) to GateCX() on qubits in [1, 2, 3]
Rule 3 (priority 70): SetQubitReadoutNoise
→ Applies ReadoutErr(0.05, 0.05) to measurements on qubits in [1]
Rule 4 (priority 80): OperationInstanceNoise
→ Applies Depolarizing(1, 0.001) to GateH() instances
Rule 5 (priority 90): GlobalReadoutNoise
→ Applies ReadoutErr(0.01, 0.02) to all measurements
Rule 6 (priority 200): IdleNoise
→ Applies AmplitudeDamping(t / 1000) to all idle qubits using relation t => ...