Sequential Quantum Gate Decomposer  v1.9.3
Powerful decomposition of general unitarias into one- and two-qubit gates gates
Examples

Sequential Quantum Gate Decomposer (SQUANDER) package provides static and shared libraries (located in the directory path/to/installed/qgd_python) that can be linked againts our own C++ code. In the forthcoming sections we provide some tricks and best practices to use the SQUANDER API. Also, the SQUANDER API is supprted by a Doxygen documentation, which can be accessed trough the file path/to/qgd/Docs/html/index.html. (To build the Doxygen documentation follow the manual at Main Page.)

A succesful build of the SQUANDER library can be tested by running the test cases in the path/to/qgd/test_standalone directory. (To build and install the SQUANDER package follow the manual at Main Page).

The expected outcome of the test program decomposition_test.cpp should look as:

decomposition_test_result.png
Expected result for the shipped test case

The output informs us that the 4-qubit unitary was decomposed by using 75 CNOT gates with decomposition error 0.000316. These information are followed by the list of the decomposing operations.

Using the SQUANDER library

In this example we show how to use the SQUANDER library for developing applications using the decomposition capabilities of the SQUANDER package. Here we explain the steps provided in the example file decomposition_test.cpp to decompose a general random 4-qubit unitary.

To include the functionalities of the SQUANDER package, provide the following lines in your code:

#include "common.h"
#include "Random_Unitary.h"
#include "logging.h"

The first include provide basic functionalities of the SQUANDER package, for example methods for aligned memory allocation and memory release. The second line includes the definition of the class to perform the decomposition of the N-qubit unitaries. This class is the key element to access the decomposition capabilities of the SQUANDER package. The third line imports the method and class definitions for creating random unitaries to be decomposed.

The SQUANDER package provides two ways to create random unitaries. The first way can be used to construct a random unitary as a product of CNOT and U3 operations. The parameters of the operations are chosen randomly, but the overall number of CNOT operations is predefined:

// The number of qubits spanning the random unitary
int qbit_num = 4;
// the number of rows of the random unitary
int matrix_size = Power_of_2(qbit_num);
// creating random unitary constructing from 6 CNOT gates.
int cnot_num = 6;
Matrix Umtx_few_CNOT = few_CNOT_unitary( qbit_num, cnot_num);

The code snippet above demonstrates the way to create a random unitary describing qbit_num qubits and constructed from cnot_num number of CNOT operations. The constructed unitary is returned through the preallocated Umtx_few_CNOT array. The second method can be used to construct general random unitaries based on the parametrization of arXiv:1303:5904v1.

// creating class to generate general random unitary
Random_Unitary ru = Random_Unitary(matrix_size);
// create general random unitary

After the creation of the unitary to be decomposed we create an instance of class N_Qubit_Decomposition to perform further calculations:

// creating the class for the decomposition. Here Umtx_adj is the complex transposition of unitary Umtx
N_Qubit_Decomposition cDecomposition =
N_Qubit_Decomposition( Umtx_adj, qbit_num, /* optimize_layer_num= */ false, /* initial_guess= */ RANDOM );

Notice, that we gave the complex transpose of the unitary Umtx as an input for the class N_Qubit_Decomposition. This can be explained by simple linear algebra considerations: since the product of the unitary with it's complex transpose ( \(U U^\dagger=I\)) gives identity, the sequence of operations bringing a unitary \(U\) into identity would naturally equal to the complex transpose \(U^\dagger\) of the unitary \(U\).

Along with the input unitary we provided two other inputs for the decomposition class.

In case we would like to try to minimize the number of CNOT gates in the decomposition, the best choice for the initial_guess values are ZEROS (discussed in more details in the forthcoming sections). However, this kind of the choice might result unwanted convergence of the optimization to local minimum instead of the global one. Thus, the solution of this example might sometimes fail to reach the global minimum. For the same reason, unitaries consisting of much CNOT gates can be well decomposed by initial guess values RANDOM or CLOSE_TO_ZERO.

Finally, before we start the decomposition, we set some other parameters for the decomposition:

// setting the number of successive identical layers used in the decomposition
std::map<int,int> identical_blocks;
identical_blocks[3] = 1;
identical_blocks[4] = 2;
cDecomposition.set_identical_blocks( identical_blocks );
// setting the maximal number of layers used in the decomposition
std::map<int,int> num_of_layers;
num_of_layers[2] = 3;
num_of_layers[3] = 12;
num_of_layers[4] = 60;
num_of_layers[5] = 240;
num_of_layers[6] = 960;
num_of_layers[7] = 3775;
cDecomposition.set_max_layer_num( num_of_layers );
// setting the number of optimization iteration loops in each step of the decomposition
std::map<int,int> num_of_iterations;
num_of_iterations[2] = 3;
num_of_iterations[3] = 1;
num_of_iterations[4] = 1;
cDecomposition.set_iteration_loops( num_of_iterations );
// setting operation layer
cDecomposition.set_optimization_blocks( 20 );
// setting the verbosity of the decomposition
cDecomposition.set_verbose( 3 );

By setting the number of identical blocks in the code snippet we order the code to use two identical successive blocks for the sub-disentanglement of the 4-qubit unitary:

layers_4qubit.png
Two identical successive block in the sub-decomposition of the 4th qubit

and do not use repeated successive blocks in the decomposition of the 3-qubit submatrix:

layers_3qubit.png
No repeated successive blocks in the sub-decomposition of the 3rd qubit

The idea behind setting two identical successive block is very straightforward. In this case the successive CNOT gates might cancel each other resulting in possible simplification of the gate structure in the end of the decomposition process. Notice, that setting more the three identical blocks has no sense, since all two-qubit unitaries can be decomposed with at most three CNOT gates.

In the second part of the code snippet above we set the maximal number of operation blocks allowed in the n-qubit sub-decomposition problem. The demonstrated choices correspond to the number of layers needed to the decomposition of general N-qubit unitaries. (These maximal parameters are in-built in the code, it is not necessary to provide them in the code.) In more specific problems the unitary might be decomposed by fewer CNOT gates. In such cases we can define the upper bond of the decomposition operation blocks via these settings.

The third part in the above code snippet is about the setting of the number of iterations in each optimization step used during the sub-decomposition of the n-th qubit. By default, the number of iteration loops are set to one, however in case of specific unitaries, it is advised to increase the number of iteration loops to avoid unwanted convergence to local minima. (On the other hand, the increase of the iteration loops might increase the running time.) We notice, that the best choice of the above parameters varies from problem to problem. One should give a try to multiple set of parameters to find the best decomposition of the unitary.

In the last command of the code snippet above one can set the verbosity of the decomposition to on/off by the value True/False. After setting the parameters of the decomposition we can start the optimization process by the command:

// starting the decomposition
cDecomposition.start_decomposition(/* prepare_export= */ true);
cDecomposition.list_gates(1);

The last command in the above code snippet prints the decomposing operations into the standard output. For further programming usage the list of decomposed operations can be retrieved via the method N_Qubit_Decomposition.get_operations of the class N_Qubit_Decomposition.

Defining custom gate structure for the decomposition

The SQUANDER package is not limited to specific choice of the gates used in the decomposition. The decomposition can be executed with arbitrary, but parameter-less two-qubit controlled gates, and with a self-designed gate staructure adopted to the underlaying hardware design as well. Here we provide a simple guide on how to define custom gate structure. A working example is provided in example file custom_gate_structure_test.cpp.

First we create a class that will hold the design of one period of the decomposing gate structure. (This period is then repeated in the quantum circuit.)

// creating an instance of the wrapper class Gates_block
Gates_block* gates_block = new Gates_block( qbit_num );

Then we can initialize the label of the qubit we need to disentangle. (SQUANDER always disentagles the qubit with the highes label.)

int disentangle_qubit = qbit_num - 1;

The custom gate structure can be constructed using a for loop:

for ( int qbit=0; qbit< disentangle_qubit; qbit++ ) {
// creating an instance of the wrapper class Gates_block
Gates_block* layer = new Gates_block( qbit_num );
if (qbit == 1) {
int target_qbit = 0;
int control_qbit = 1;
// add U3 gate to the block
bool Theta = true;
bool Phi = false;
bool Lambda = true;
layer->add_u3( 0, Theta, Phi, Lambda );
layer->add_u3( 1, Theta, Phi, Lambda );
// add CNOT gate to the block
layer->add_cnot( target_qbit, control_qbit );
}
else {
int target_qbit = qbit;
int control_qbit = disentangle_qubit;
// add U3 gate to the block
bool Theta = true;
bool Phi = false;
bool Lambda = true;
layer->add_u3( qbit, Theta, Phi, Lambda );
layer->add_u3( disentangle_qubit, Theta, Phi, Lambda );
// add CNOT gate to the block
layer->add_cnot( target_qbit, control_qbit );
}
gates_block->add_gate( (Gate*)layer );
}

In the individual layers (defined again by an instance of class Gates_block) we can choose between qubits to play the role of the control and the target qubit on demand. Whatever is our choice, we should apply U3 gates to the qubits in front of the chosen two-qubit controlled gate. The U3 gates should have two free parameters, lets say variable Theta and Lambda, while parameter Phi is kept constant zero during the decompsition. (Parameters set to True are free parameters, while parameters set to False are kept constant.) In the first "if" body a connection between qubits 0 and 1 is created via a CNOT gate. In particular, qubit 0 is chosen as the target qubit, and qubit 1 is chosen to be the target qubit. (The oriantation of the two-qubit controllod gates can be reversed, so the specific choice of the roles of the target and control qubits is not important in principle.)

The second "if" body controls the gate structure between any other qubit pairs. The created layers are added to the gates_block which can be used to decompose a quantum program. The figure below shows the period of the constructed gate structure in case of three qubits

custom_structure.png
A custom three-qubit gate structure

We continue by creating a random 3-qubit unitary (and its transpose conjugate) by code snippet:

// The number of qubits spanning the random unitary
int qbit_num = 3;
// the number of rows of the random unitary
int matrix_size = Power_of_2(qbit_num);
// creating class to generate general random unitary
Random_Unitary ru = Random_Unitary(matrix_size);
// create general random unitary
// construct the complex transpose of the random unitary
Matrix Umtx_adj = Matrix(matrix_size, matrix_size);
for (int element_idx=0; element_idx<matrix_size*matrix_size; element_idx++) {
// determine the row and column index of the element to be filled.
int col_idx = element_idx % matrix_size;
int row_idx = int((element_idx-col_idx)/matrix_size);
// setting the complex conjugate of the element in the adjungate matrix
QGD_Complex16 element = Umtx[col_idx*matrix_size + row_idx];
Umtx_adj[element_idx].real = element.real;
Umtx_adj[element_idx].imag = -element.imag;
}

Then, we create an instance of a class to decompose the unitary

// creating the class for the decomposition. Here Umtx_adj is the complex transposition of unitary Umtx
N_Qubit_Decomposition cDecomposition =
N_Qubit_Decomposition( Umtx_adj, qbit_num, /* optimize_layer_num= */ false, /* initial_guess= */ RANDOM );

and we set the constructed gate structure to be used in the decomposition

// creating custom gate structure for the decomposition
std::map<int, Gates_block*> gate_structure;
gate_structure.insert( pair<int, Gates_block*>(qbit_num, create_custom_gate_structure( qbit_num ) ) );
gate_structure.insert( pair<int, Gates_block*>(qbit_num-1, create_custom_gate_structure( qbit_num-1 ) ) );
// setting the custom gate structure in the decomposition class
cDecomposition.set_custom_gate_structure( gate_structure);
// release the custom gate structure since it was cloned by cDecomposition
delete gate_structure[qbit_num];
delete gate_structure[qbit_num-1];

In order to disentangle the 5-th, 4-th, 3-rd or the n-th qubit we may use different gate structure, each of them should be defined as a pair element in a C++ map gate_structure. Finally, we start to decompose the unitary and show the results.

// starting the decomposition
cDecomposition.start_decomposition(/* finalize_decomposition = */ true, /* prepare_export= */ true);
cDecomposition.list_gates(1);