Algorithms for code concatenation
My goal is to compute fault-tolerance thresholds for stabililzer codes. To this end, we need to understand how to concatenate quantum codes. Briefly, code concatenation is when a state is encoded first by one code, then the encoded qubits again by same or another code. The resultant qubits may be encoded again by another code, and so on. The entire concatenation of codes can be treated as a single code, which has a larger number of physical qubits than any of the individual codes but also a larger a distance.
In our study of code concatenation, we are interested in the following:
- What are the generators of the concatenated code?
- How to perform encoding and logical operations of the code?
- How to do error correction.
Today, I will be mostly interested in the first question. My development will follow the algorithms presented by Gaitan [1], though I have made significant advances in the clarity of the arguments.
First foray: a two layer concatenation
Suppose, we encode qubits using the code . This yields encoded qubits. We then take each of these qubits, and encode each one separately using . We will refer to the concatenated code as .
This process can be visualized as follows. The numbers indicate how many elements are in the box to their right.
Let's first consider the question of generators. The generators of are and are . What are the generators of ? To answer this, let be a state of qubits that is to be encoded. The first encoding is written as We note for now that for all ,
Now, for the second round of encoding, we will separate each of the qubits and encode each one using . To write this, let . Then When we encode each qubit, we obtain, where the bars in indicate that it is an encoded state, and the subscript indicates that it has qubits. To summarize, let us call each block of qubits , where because there are such blocks.
Generators
In this way, our doubly encoded state is a qubit state. Overall, we have encoded qubits into qubits. This means that the concatenated code must have generators. Let's find them.
(1) Consider a generator of , which is a qubit operator, but we have to be careful over which qubits it acts. For instance, we can write to obtain an operator that acts on the first qubits of total qubits. Similarly, we can write to obtain an operator that acts on the next qubits. This notation will get tedious fast, so let's just write to indicate that acts on qubits , where .
Now, it's quite easy to see from definition that for all , i.e. if is made to act on the -th block of qubits in the final block, it will be a stabilizer operation. In other words, we have discovered that the set stabilizes the state . We can count that there are such operators.
(2) But wait, there are more operators that stabilize the state . Let be the encoding operation for . Then is the encoding operations for the second step. Conversely, is the decoding operation. Then, . We noted earlier that stabilizes this state. Hence,
Hence, the set must also stabilize the doubly encoded state. There are such operators.
In total, we have stabilizers. As we have found sufficient number of stabilizers, we are done.
How to put this mathematics into practice
Let's try to construct the algorithm that creates the generators of the concatenated code. We will depend on the vector representation of the Pauli operators.
(1) Constructing the first set of generators is very simple. We just have to do some shifting. Note that , an operator on qubits, is represented by a vector of length . We have to construct, , an operator over qubits, which is represented by a vector of length . The code is right there is the notation.
Call . Then
# for each of the E(b) blocks for b in range(n_1): for each g: encoded_g = zeros((1,2*n)) # for the X part encoded_g[n_2*b: n_2*(b+1)] = g[:n2] # for the Z part encoded_g[n + n_2*b: n + n_2*(b+1)] = g[n2:]
(2) For the second set, we have to do a little bit of work to understand encoding. If we use to encode a qubit, it takes the logical operators of the qubit and maps them to the encoded . In vector notation, and and these are mapped to vectors of length . This mapping is already known and we have constructed it before.
Now, we have which is an operator over qubits, which means it is the product of different Pauli operators, i.e. . It is represented by a vector of length . So Our algorithm is now clear. We iterate over the operators in and encode each according to the mapping . We have to be careful about the shifting as before, because acts on qubits to .
# declare # logical_x # logical_z for each g: encoded_g = zeros((1,2*n_1*n_2)) for i in range(n1): if g[i] and g[n+i]: #P_i is the Y operator # multipling operators corresponds to adding their vectors mod 2 encoded_g[n2*i: n2*(i+1)] += logical_x[:n2] + logical_z[:n2] encoded_g[n + n2*i: n + n2*(i+1)] += logical_x[n2:] + logical_z[n2:] elif g[i]: # P_i is the X operator encoded_g[n2*i: n2*(i+1)] += logical_x[:n2] encoded_g[n + n2*i: n + n2*(i+1)] += logical_x[n2:] elif g[n+i] # P_i is the Z operator encoded_g[n2*i: n2*(i+1)] += logical_z[:n2] encoded_g[n + n2*i: n + n2*(i+1)] += logical_z[n2:]
That wasn't too difficult.
does not have to be , but divides
Let us now relax the assumption that , but let it be an interger that divides . This will complicate the encoding in the second round. As before, the first round is Because is divisible by , these qubits can be divided into blocks, each of size . Let the blocks of qubits be called for . Let , and let be the basis of a dimensional Hilbert space. We can write the first-round encoded state as broken up into a sum over -qubit basis states.
Now, we encode each block using , to create a new encoded block of size . This creates the final state This final state has blocks each of size qubits, for a total of qubits. Hence, in this code qubits are encoded to qubits, which is fewer than before.
The structure of the generators is similar to what we had above.
(1) If we apply the operator to the -th encoded block , when the qubits are in state we find that it stabilizes the state. Hence, it must be a stabilizer of the doubly encoded state. As before the sized operator when acting on the -th block is labelled . It's algorithmic implementation is as before, except the change in the number of blocks .
(2) What about the associated generators? Things are a bit more convoluted because of the grouping of the qubits. Now, note that is an operator that takes qubits to qubits, and in the second encoding, we apply to each group of qubits. This means that Hence, our stabilizers are given by
How do we process ? If, as before, , then we break it up according to into chunks of , to form Then,
What do these arcane incantations mean? Just concentrate on one to-be-encoded block B(b), which has qubits. qubits have many operators, one for each qubit; many operators, etc. encodes these into encoded operators. So
This means that in the operators acting on , the -th operator is encoded to . This results in , which act on the block.
Recall that the block corresponds to qubits and corresponds to qubits .
The algorithm acquires an additional loop over the blocks
# declare # logical_xs # logical_zs encoded_g = zeros((1,2*n)) # iterate over each block for b in range(n1/k2): # extract the B(b) block from the generator g_block_x = g[k2*b: k2*(b+1)] g_block_z = g[n1+k2*b:n1+k2*(b+1)] # now iterate over the qubits in the block for i in range(k2): if g_block_x[i] and g_block_z[i]: #P_i is the Y operator # place the $i$-th encoded logical gate into the E(b) block in the encoded_g encoded_g[n2*b: n2*(b+1)] += logical_xs[i][:n2] + logical_zs[i][:n2] encoded_g[n + n2*b: n + n2*(b+1)] += logical_xs[i][n2:] + logical_zs[i][n2:] elif g_block_x[i]: # P_i is the X operator encoded_g[n2*b: n2*(b+1)] += logical_xs[i][:n2] encoded_g[n + n2*b: n + n2*(b+1)] += logical_xs[i][n2:] elif g_block_z[i] # P_i is the Z operator encoded_g[n2*i: n2*(b+1)] += logical_zs[i][:n2] encoded_g[n + n2*b: n + n2*(b+1)] += logical_zs[i][n2:]
Stac implements these algorithms, so you can concatenate arbitrary codes. Here is the example from the book reproduced. The code is concatenated with itself. Note that and so divides .
import stac
import numpy as np
# define the code
cd = stac.CommonCodes.generate_code('[[4,2,2]]')
# book uses a different set of logical operators
# than stac computes using the Gottesman method
cd.logical_xs = np.array(
[[1, 0, 1, 1, 0, 0, 1, 1],
[1, 0, 1, 0, 0, 0, 0, 1]])
cd.logical_zs = np.array(
[[1, 0, 1, 0, 1, 1, 1, 0],
[0, 1, 0, 0, 0, 0, 1, 1]])
# this will create a new code object,
# and immediately compute the generator matrix
concat_code = stac.ConcatCode((cd, cd))
# display the generator matrix
stac.print_paulis(concat_code.generator_matrix)
# answer is correct
Let not divide
Things are getting a bit more complicated. If we encode a block of qubits into a block of using , then there is no way of dividing up the qubits into -sized blocks that can be encoded using .
(1) The solution is to start with blocks of qubits each, which we will label as q(c), for . So our initial state will be where there are terms in the product. We have a total of qubits. Note that this steps means we have given up on the ability to encode an unknown quantum state, for which we only have one copy. However, for quantum computational algorithms, we rarely start with an unknown state, so this is a not a big impediment.
(2) Next, we encode each block using into a block of qubits. We obtain which has a total of qubits.
(3) For the next step, we will need to rearrange the qubits. We are going to pick the first qubit in each and make that one block, take the second qubit in each and make those a block, and so on. Each new block will be called . Since, there are many blocks, each block will of size as well, And because the size of is , there will be many blocks, with .
To make this explicit, we operate on the states we created above. First, we expand one term in the product above in some basis to obtain Then, We can now rearrange these to obtain, To summarize, this state has blocks of size for a total of qubits.
(4) We can now encode each block using . Hence, This state now has blocks of size , for a total of qubits.
In summary, this concatenated code encodes qubits into qubits.
Obtaining the generators
We are now going to determine the generators of the final encoded state. Note, that we need a total of generators.
(1) As before, the generators of must by definition stabilize the state of each block ,
Hence, we assign a copy of all generators for each block to our collection. This means generators. The algorithm is the same, except for the number of blocks.
(2) The encoding of the generators is much more subtle. If is applied to a block, it stabilizes the state . But in the subsequent re-ordering, is broken up. To start with an example, consider acting on , Where do the qubits that these act on go after the re-ordering. Since, this is the -th block, now acts on the -th position in the block. Subsequently, when we encode each using , each of the will map the the corresponding -th logical operator. Meaning if , then after encoding it should be . In the same way, if acts on , then all its constitutive Pauli operators go to the -th location in and hence should be mapped to .
The algorithm for this is as follows:
# declare # logical_xs # logical_zs # iterate over each block Q(c) for c in range(k2): for each g: encoded_g = zeros((1,2*n)) # now iterate over the qubits in g for i in range(n1): if g[i] and g[n+i]: #P_i is the Y operator # place the c-th encoded logical gate into the E(b) block in the encoded_g encoded_g[n2*i: n2*(i+1)] += logical_xs[c][:n2] + logical_zs[c][:n2] encoded_g[n + n2*i: n + n2*(i+1)] += logical_xs[c][n2:] + logical_zs[c][n2:] elif g[i]: # P_i is the X operator encoded_g[n2*i: n2*(i+1)] += logical_xs[c][:n2] encoded_g[n + n2*i: n + n2*(i+1)] += logical_xs[c][n2:] elif g[n+i] # P_i is the Z operator encoded_g[n2*i: n2*(i+1)] += logical_zs[i][:n2] encoded_g[n + n2*i: n + n2*(i+1)] += logical_zs[c][n2:]
We can also reproduce the example from the book, in which and .
cd2 = stac.CommonCodes.generate_code('[[5,1,3]]')
concat_code = stac.ConcatCode((cd2, cd))
stac.print_paulis(concat_code.generator_matrix)
Stac can just put the two algorithms together and concatenate an arbitrary number of codes.
concat_code = stac.ConcatCode((cd, cd2, cd2))
print(concat_code)
A [[100,2]] code
A final note about distance
The book shows that if has distance and has distance , then the concatenated code has distance at least . We will look into this more next time.
[1] Gaitan, Frank, Quantum Error Correction and Fault Tolerant Quantum Computing (CRC Press, 2008).