Miscellaneous Features
In this tutorial we cover some of the features QXTools
has to improve quantum circuit simulations and how to specify the type of output we would like our simulations to generate.
Hypergraphs
One feature that QXTools
uses by default is that of hypergraphs. The hypergraph for a given tensor network is a graph, representing the network topology, whose nodes correspond to tensors in the network and edges correspond to indices in the network that connect different tensors. Note, two indices in the netwrok may be associated with the same edge in the hypergraph if they both connect to a diagonal tensor and are identical as a result. The tensor network topology can also be represented using an ordinary graph but it typically requires a greater number of edges to do so.
This is useful because to find an efficient contraction plan for a given tensor network, QXTools
first creates a line graph for the network topology and then runs the FlowCutter algorithm to find a tree decompostion for that line graph which can be converted into a contraction plan. Running FlowCutter on smaller graphs often results in more efficient contraction plans, with a smaller treewidth, in a shorter amount of time.
QXTools
, together with QXTns, can automatically detect the diagonal structure of gate tensors in a network and create the line graph of a network while taking the hypergraph structure of the network into account. We illustrate this below by creating the line graph for a network both with and without taking the hypergraph structure into account and show that using the hypergraph structure results in a smaller line graph. Below, the line graph is represented by the LabeledGraph data structure implemented by the QXGraphDecompositions package. @show
-ing this object should display the number of vertices and edges in the graph.
# Create a random circuit.
tnc = TensorNetworkCircuit(3)
CX = [[1., 0., 0., 0.] [0., 1., 0., 0.] [0., 0., 0., 1.] [0., 0., 1., 0.]]
for layer = 1:10
push!(tnc, [2, 1], CX)
push!(tnc, [2, 3], CX)
push!(tnc, [2], rand(2, 2))
end
# Create the line graph for the circuit without using the hypergraph.
# The number of vertices and edges are displayed respectively.
lg, symbol_map = convert_to_line_graph(tnc, use_hyperedges=false)
@show lg.graph
# Create the line graph for the circuit using the hypergraph.
lg, symbol_map = convert_to_line_graph(tnc, use_hyperedges=true)
@show lg.graph
Slicing
For simulations that use a contraction plan with a large treewidth, it may be the case that the simulation requires more memory than is available on the machine running the simulation. In this situation, it is necessary to reduce the memory requirements of the simulation.
The standard method for reducing the memory requirements of a simulation in exchange for greater time complexity is known as slicing. This method works by fixing the value of selected indices in the network to decompose the contraction of the network into the sum of contractions of smaller networks. For more details on this method see the section on slicing in this article.
For a given contraction plan for a tensor network, QXTools
uses Shutski's tree-trimming, which is implemented in the package QXGraphDecompositions to select efficient indices of a network to slice.
To find a contraction plan for a quantum circuit using flowcutter and then slice a specific number of indices using Shutski's tree trimming method, the convenience function contraction_scheme
can be used and is demonstrated below.
# Create a dummy circuit to test on.
N = 5
CX = [[1., 0., 0., 0.] [0., 1., 0., 0.] [0., 0., 0., 1.] [0., 0., 1., 0.]]
tnc = TensorNetworkCircuit(N)
for layer = 1:10, qubit = 1:N-1
push!(tnc, [qubit, qubit+1], CX)
end
add_input!(tnc)
# Use FluwCutter for 10 seconds to find a contraction plan and then select 8 indices for slicing
# using the tree trimming method.
indices_to_slice, contraction_plan, metadata = contraction_scheme(tnc, 8; time=10, hypergraph=true)
println("The number of indices to be sliced is ", length(indices_to_slice))
println("The treewidth as a function of the number of indices sliced is ",
metadata["Slicing"]["Treewidths after slicing consecutive edges"])
QXContexts output
The output of a distributed simulation run by QXContexts is determined by output parameters in the simulation files generated by the generate_simulation_files
function in QXTools
. To dictate which output is generated by QXContexts
, a dictionary containing parameters for the desired output method needs to be passed to generate_simulation_files
. To this end, QXTools
provides the function output_params_dict
which will create the relevant dictionary for the desired output. The tree output methods available in QXContexts are described below. Note, for each method, output_params_dict
takes two positional arguments: num_qubits
and num_outputs
giving the number of qubits in the circuit to be simulated and the desired number of outputs to generate (ie number of bitstring samples or number of amplitudes to compute).
Rejection
To generate random bitstrings, which are distributed as if they were measured on an ideal, noiseless quantum device running our quantum circuit, rejection sampling can be used. To create the output dictionary for rejection sampling, we can use the following keyword arguments of output_params_dict
.
num_qubits = 5
num_outputs = 5
output_params = output_params_dict(num_qubits, num_outputs;
output_method = :Rejection,
M = 0.0001,
fix_M = false,
seed = 42)
Here, M
is the parameter of rejection sampling that determines the average number of rejections made before a candidate output is accepted. QXContexts implements empirical supremum rejection sampling where M
should initially be chosen to be small and is subsequently updated to larger values (based on the largest probability amplitude computed) every iteration of the sampling method. Updates to the value of M
may be disabled by setting fix_M = true
. This is useful when a particular value of M
is desired as is in frugal rejection sampling.
List
To compute the probability amplitudes for a list of bitstrings the list method can be used. In this case, we need only pass an array containing the bitstrings we want amplitudes for as a keyword argument to output_params_dict
.
num_qubits = 5
num_outputs = 3
bitstrings = ["1111", "1101", "0101"]
output_params = output_params_dict(num_qubits, num_outputs;
output_method = :List,
bitstrings = bitstrings)
Uniform
To compute the probability ampltiudes for a number of uniformly random bitstrings we can use the uniform method. The following will generate an outputs dictionary which will get QXContexts
to generate num_outputs
uniformaly random bitstrings for a quantum circuit with num_qubits
qubits and compute probabiltiy amplitudes for them.
num_qubits = 5
num_outputs = 3
output_params = output_params_dict(num_qubits, num_outputs;
output_method = :Uniform,
seed = 42)
Example
All paramters discussed above can be passed to the generate_simulation_files
function. For a given QXZoo circ
object, this function will create TensorNetworkCircuit object for the circuit, find a contraction plan for the associated tensor network using FlowCutter, determine the desired number of indices to slice and then generate a set of simulation files describing what output QXContexts
should generate and how it should contract the relevant tensor networks. For the following example, it is assumed a circ
object has already been created.
# The prefix to be used for the names of the simulation files
output_prefix = "my_cirq_sim"
# Paramters for FlowCutter and the tree-trimming method for slicing.
time = 10
hypergraph = true
number_indices_to_slice = 8
# An output dictionary describing the desired output from the simulation.
num_qubits = circ.num_qubits; num_outputs = 15
output_args = output_params_dict(num_qubits, num_outputs; output_method = :Uniform, seed = 42)
# Generate the files describing how the simulation should be executed by QXContexts.
generate_simulation_files(circ,
output_prefix,
number_indices_to_slice;
seed = 42,
output_args = output_args,
time = time,
hypergraph = hypergraph)