--- jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 jupytext_version: 1.14.1 kernelspec: display_name: Python 3 language: python name: python3 --- # Noise Scaling Methods In this tutorial we will compare two noise scaling methods available for use in [Zero-Noise Extrapolation](https://mitiq.readthedocs.io/en/stable/guide/zne.html) (ZNE): identity insertion and unitary folding. ZNE works by running multiple versions of the desired circuit, each intended to scale the noise up from the base-level achieved by the hardware. Experimentally these experiments are often performed by pulse stretching, but as a quantum programmer, we typically do not have access to such low-level control. For this reason, we use "digital" methods that allow us to scale the noise using gate-based methods. To this end, we will study circuit folding and identity insertion as methods to increase the amount of noise present in our computation. These techniques are summarized by the following equations, and can be performed in Mitiq with the associated functions. | | Folding | Identity Insertion | | -------------- | -------------------- | ------------------ | | Equation | $G \to GG^\dagger G$ | $G \to I G$ | | Mitiq Function | [fold_global](https://mitiq.readthedocs.io/en/stable/apidoc.html#mitiq.zne.scaling.folding.fold_global) | [insert_id_layers](https://mitiq.readthedocs.io/en/stable/apidoc.html#mitiq.zne.scaling.identity_insertion.insert_id_layers) | ## Comparison To get started, we can demo what these two functions do to a small GHZ circuit. Each function (fold_global and insert_id_layers) will take a circuit, and a specified scale factor as inputs. The argument scale_factor controls how much to increase the depth of the input circuit so that the achieved scale factor is exactly equal, or very close, to the specified scale factor. {code-cell} ipython3 from mitiq.benchmarks import generate_ghz_circuit from mitiq.zne.scaling import insert_id_layers, fold_global demo = generate_ghz_circuit(3) scale_factor = 3 print("-----ORIGINAL-----") print(demo) print("\n-----------------FOLDING------------------") print(fold_global(demo, scale_factor)) print("\n-----------------SCALING------------------") print(insert_id_layers(demo, scale_factor))  Theoretically, these circuits should give the same result when measured, but due to noise, this is almost never the case. Both methods work by extending the duration of the circuit, but do so in different ways that might be beneficial for different scenarios. When using folding, noise is amplified by applying additional gates, and in particular inverse gates. Scaling amplifies noise by letting the qubits idle for longer _between_ computation. {warning} Unitary folding scales noise by applying an additional layer $G^\dagger G$ to the circuit. For non-hermitian gates $G$ and $G^\dagger$ may not have the same noise model, and hence noise is potentially scaled in an non-linear way. Similarly, the noise that predominantly scaled in identity insertion is that of idle qubit noise/decoherence.  Let's now look at how much the depth of these circuit increase with different scale factors. {code-cell} ipython3 print( "{: >12} {: ^14} {: ^14} {: ^15}".format( "scale factor", "original depth", "folded depth", "id insertion depth" ) ) for scale_factor in range(1, 10): folded_depth = len(fold_global(demo, scale_factor)) id_insert_depth = len(insert_id_layers(demo, scale_factor)) print( "{: >12} {: ^14} {: ^14} {: ^15}".format( scale_factor, len(demo), folded_depth, id_insert_depth ) )  As expected, we have $\mathtt{depth} * \mathtt{scale\_factor} = \mathtt{scaled\_depth}$ when the scale factor is an integer. The scale factor can also take on non-integer values, where this equation will hold approximately. ## Using noise scaling methods Here, we demo how you can use these noise-scaling technique in ZNE. First, we define an [executor](https://mitiq.readthedocs.io/en/stable/guide/executors.html) which is needed to tell Mitiq how to run the circuit. We choose depolarizing noise via a simple density matrix simulation to act between every circuit layer. {code-cell} ipython3 from mitiq.zne import execute_with_zne import cirq def execute(circuit, noise_level=0.05): noisy_circuit = circuit.with_noise(cirq.depolarize(p=noise_level)) return ( cirq.DensityMatrixSimulator() .simulate(noisy_circuit) .final_density_matrix[0, 0] .real )  We can then pass the desired noise-scaling method into execute_with_zne using the scale_noise keyword argument. {code-cell} ipython3 execute_with_zne(generate_ghz_circuit(6), execute, scale_noise=insert_id_layers)  To give a slightly more systematic understanding of the differences between these two methods, we will perform a small experiment on the following variational circuit. {code-cell} ipython3 def variational_circuit(gamma): q0, q1 = cirq.LineQubit.range(2) return cirq.Circuit( [ cirq.rx(gamma)(q0), cirq.CNOT(q0, q1), cirq.rx(gamma)(q1), cirq.CNOT(q0, q1), cirq.rx(gamma)(q0), ] )  We can run this circuit many times, varying $\gamma$ each time, and compute ideal and noisy expectation values to compare to the mitigated versions. We do this comparison by using an improvement factor (IF) which is defined as {math} \left|\frac{\text{ideal} - \text{noisy}}{\text{ideal} - \text{mitigated}}\right|.  {code-cell} ipython3 from random import uniform import numpy as np results = {"fold": [], "id": []} for _ in range(100): gamma = uniform(0, 2 * np.pi) circuit = variational_circuit(gamma) ideal_expval = execute(circuit, noise_level=0.0) noisy_expval = execute(circuit) folded_expval = execute_with_zne(circuit, execute, scale_noise=fold_global) id_expval = execute_with_zne(circuit, execute, scale_noise=insert_id_layers) noisy_error = abs(ideal_expval - noisy_expval) folded_IF = noisy_error / abs(ideal_expval - folded_expval) scaled_IF = noisy_error / abs(ideal_expval - id_expval) results["fold"].append(folded_IF) results["id"].append(scaled_IF) print("Avg improvement factor (fold_global): ", round(np.average(results["fold"]), 4)) print("Avg improvement factor (insert_id_layers): ", round(np.average(results["id"]), 4))  As we can see, both techniques offer an improvement, but for this problem identity insertion outperforms folding dramatically. This is due to the fact that ZNE assumes one can scale the noise throughout a circuit by doubling, tripling, etc, the noise's strength. On the simple noise model used in our example, identity insertion does exactly this, and hence has great performance. Noise models with increased complexity and gate-dependent errors would likely see more equal, or perhaps better performance by folding-based techniques. ## Conclusion In this tutorial, we've shown how to use both folding and identity insertion as noise scaling methods for Zero-Noise Extrapolation. If you're interested in finding out more about these techniques, check out our [Noise Scaling Functions](https://mitiq.readthedocs.io/en/stable/guide/zne-3-options.html#noise-scaling-functions) section of our users guide!