# Example: Randomized Compilation with different Compilation Options

Randomized Compiling in True-Q™ can be configured in various ways. For example, the Example: Customizing Randomized Compiling with Cycle Markers example shows how the user can specify which cycles in a circuit are hard cycles and thus subject to the random twirls that the randomly_compile() inserts around those hard cycles. Three other important configuration options include:

1. Measurement randomization through additional Paulis

An effective way to reduce readout bias is to randomize the measurements by performing a random Pauli operation just before each measurement instruction. This can be added to Randomized Compiling by specifying the compile_paulis argument.

2. Randomized Compiling for non-Clifford entangling gates

Randomized Compiling can be used for circuits that contain hard cycles with gates which are not from the Clifford group by specifying the entangler argument.

3. Specifying the action on idle qubits

In systems with large single-qubit errors it might be desirable to not introduce any additional single-qubit gates on what would otherwise be idle qubits. This compilation option can be specified through the Compiler passes: RCCycle and RCLocal.

The following examples show how these configuration options work in practice.

Note

Randomized Compiling produces a new circuit collection after each call, so the output of this example will be different if it’s executed again.

[2]:

import numpy as np

import trueq as tq
import trueq.compilation as tqc
from trueq.algorithms import qft


## Measurement Randomization through Additional Paulis

To remove readout bias, we can randomize measurements using the keyword argument compile_paulis of the randomly_compile() function. This argument is set to False by default because it changes the measurement outcomes which can confuse new users. When set to True, a single-qubit cycle of randomly selected Paulis (or Weyls in the case of qudits) will be added right before each measurement operation in a randomly compiled circuit and merged into any preceding single-qubit gate cycle. Take a look at the corresponding compiler pass CompilePaulis for more information.

Let’s walk through a quick example. We begin by creating a simple circuit to play with and generate a randomly compiled version of that circuit:

[3]:

cycle1 = {0: tq.Gate.h}
cycle2 = {(0, 1): tq.Gate.cx}
cycle3 = {2: tq.Gate.h}
cycle4 = {(2, 3): tq.Gate.cx}

circuit = tq.Circuit([cycle1, cycle2, cycle3, cycle4])
circuit.measure_all()
circuit.draw(interactive=False)

[3]:

[4]:

rc_circuits = tq.randomly_compile(circuit, compile_paulis=True)

# Select one circuit from the collection
rc_circuit = rc_circuits[0]

rc_circuit.draw(interactive=False)

[4]:


When we compare the outcomes of the original circuit and the randomly compiled circuit, we are likely to observe different bitstrings:

[5]:

# instantiate a simulator and run the original and the RC circuit
sim = tq.Simulator()

original_result = sim.sample(circuit, n_shots=np.inf)
rc_result = sim.sample(rc_circuit, n_shots=np.inf)

tq.visualization.plot_results(
original_result,
rc_result,
labels=["Original Circuit", "RC Circuit with compiled Paulis"],
)


The Pauli operators that were added to the RC circuit are stored in the circuit’s key attribute as a Weyls instance:

[6]:

rc_circuit.key

[6]:

Key(compiled_pauli=Weyls('ZIXZ'), protocol='RC', twirl=Twirl({(0,): 'P', (1,): 'P', (2,): 'P', (3,): 'P'}, dim=2))


To recover the original bitstrings, we can apply the decompiled_results() method to the result with the compiled_pauli key from the circuit:

[7]:

compiled_paulis = rc_circuit.key.compiled_pauli

rc_result_decompiled = rc_result.decompiled_results(compiled_paulis)

tq.visualization.plot_results(
original_result,
rc_result_decompiled,
labels=["Original Result", "RC Result decompiled"],
)


When averaging over the results of multiple RC circuits in a collection, the sum_results() function can automatically decompile the results. This behavior can be adjusted by changing the value of the decompile_paulis argument. By default, it is set to True which in most cases yields the desirable outcome:

[8]:

# run all RC circuits on the simulator
sim.run(rc_circuits)

# average the decompiled results
rc_circuits.sum_results(decompile_paulis=True).normalized()

[8]:

Results({'0011': 0.25066666666666665, '0000': 0.2553333333333333, '1111': 0.238, '1100': 0.256})


As you can see, the averaged results contain only the bitstrings that were in the original set of results, since the result of every RC circuit in the collection has been decompiled.

## Randomized Compiling for non-Clifford Entangling Gates

The trueq.randomly_compile() function by default assumes that all hard cycles in the input circuit consist purely of Clifford gates. This is because under the standard Pauli twirl, the correction gates for the twirl gate can always be expressed as single-qubit gates which can be compiled into neighboring easy cycles such that the overall circuit depth is maintained [11].

It is possible to twirl a cycle that has non-Clifford gates in two ways. One solution is to customize the set of twirling gates. The other approach, as shown here, is to use True-Q™'s built-in Compiler to convert the cycle into a Clifford-based representation. This can be done either implicitly by specifying the entangler argument of the trueq.randomly_compile() function, or explicitly through defining a custom Compiler. Let’s take a look at both options.

As a concrete example, consider the Quantum Fourier Transform (QFT) circuit, that is commonly expressed in terms of the $$CROT$$ gate (where $$CROT(\phi):=\exp(i \phi |11 \rangle \langle 11|)$$):

[9]:

# define a QFT circuit on 4 qubits
qft_circuit = qft(range(4))
# display the output
qft_circuit.draw()

[9]:


In order to produce randomly compiled versions of this circuit, we need to replace these non-Clifford entangling gates with a Clifford gate, such as the $$CX$$ gate. To tell the trueq.randomly_compile() function to use $$CX$$ gates for the compilation, we specify the entangler argument in the function call as follows:

[10]:

rc_circuits = tq.randomly_compile(qft_circuit, entangler=tq.Gate.cx)

# display the first circuit in this collection:
rc_circuits[0].draw()

[10]:


Note that this circuit (and all other circuits in this collection) now consists purely of Clifford gates, and the entangling gates are all $$CX$$ gates, while implementing the same logical operation as the original circuit.

When the entangler argument is specified, the trueq.randomly_compile() function will first create a new circuit that only uses the specified entangling gate, and then generate randomly compiled versions of that circuit. This happens implicitly in the background. If desired however, it can also be done explicitly through creating a custom Compiler:

[11]:

entangler = tq.Gate.cx
# Define a pass for the compiler that specifies which two-qubit gates to use
entangler_pass = tqc.Native2Q([tq.config.GateFactory.from_matrix("CX", entangler.mat)])
# define the compiler
compiler = tqc.Compiler(passes=[entangler_pass, tqc.Merge()])

# compile the QFT circuit
compiled_qft_circuit = compiler.compile(qft_circuit)
# display the resulting circuit
compiled_qft_circuit.draw()

[11]:


This circuit implements the same operation as the original QFT circuit but all its entangling gates have been replaced by a combination of single-qubit gates and two-qubit $$CX$$ gates.

Note

For more information on the usage of True-Q™'s Compiler take a look at the Example: Compilation Basics and Example: Defining Custom Compilers examples.

Since this compiled circuit consists purely of Clifford gates, we can call the trueq.randomly_compile() function on it without any further arguments:

[12]:

rc_circuits = tq.randomly_compile(compiled_qft_circuit)
# display a sample circuit from this collection:
rc_circuits[0].draw()

[12]:


Both methods, i.e. using the entangler argument and compiling the circuit explicitly, yield the same result and allow for the generation of randomly compiled circuits of an input circuit with non-Clifford entangling gates.

## Specifying the Action on Idle Qubits

By default, the trueq.randomly_compile() function will insert a single-qubit twirling and a matching compensation gate around every hard cycle in the circuit. There are however applications in which only a subset of the qubits is involved in the algorithm in a given cycle while the other qubits remain idle. If, in addition to that, the computation is performed on a system with non-negligible single-qubit errors, it might be desirable to not apply any extra gates to those qubits which would otherwise be idle.

This can be achieved through performing the compilation of the input circuit explicitly by defining a custom :py:class~trueq.compilation.Compiler and specifying the Randomized Compiling Pass manually. There are two options:

1. RCCycle: this is the default Pass that performs Randomized Compiling on a set of input cycles by inserting single-qubit gates on either side of the cycles.

2. RCLocal: this Pass also performs Randomized Compiling on a set of input cycles, however the single-qubit gates are only added to qubits that the cycles they surround act on; idling qubits are left alone.

Let’s take a look at a concrete example. Consider the follwing circuit of stacked $$CX$$ gates that has two idling qubits in each cycle:

[13]:

circuit = tq.Circuit(
[
{0: tq.Gate.h, 3: tq.Gate.h},
{(0, 1): tq.Gate.cx},
{(1, 2): tq.Gate.cx},
{(2, 3): tq.Gate.cx},
]
)
circuit.draw()

[13]:


We can apply our default Randomized Compiling to this circuit through the following Compiler configuration (and the tq.randomly_compile() would produce the same result):

[14]:

rc_cycle_compiler = tqc.Compiler(
passes=[
# specify a pass to mark hard cycles:
tqc.MarkCycles(),
# specify the main RC pass:
tqc.RCCycle(),
# merge single-qubit cycles together:
tqc.Merge(),
]
)
# generate one randomly compiled circuit using this compiler:
rc_cycle_circuit = rc_cycle_compiler.compile(circuit)
rc_cycle_circuit.draw()

[14]:


Note that with the RCCycle pass, every hard cycle is now fully surrounded by single-qubit gates.

In contrast, when we apply the RCLocal pass, single-qubit gates are only applied to qubits that the hard cycles act on:

[15]:

rc_local_compiler = tqc.Compiler(
passes=[
# specify a pass to mark hard cycles:
tqc.MarkCycles(),
# specify the main RC pass:
tqc.RCLocal(),
# merge single-qubit cycles together:
tqc.Merge(),
]
)
# generate one randomly compiled circuit using this compiler:
rc_local_circuit = rc_local_compiler.compile(circuit)
rc_local_circuit.draw()

[15]: