The Anti-Infinity Parade

Oct 08, 2022

Encoding circuits for stabilizer codes and the Steane code

Yesterday we introduced the Steane code. One thing we did was mathematically construct the logical basis states for Steane code. A natural question to ask is, how does one create that state using a quantum circuit?

Gottesman's method to construct logical zero state of stabilizer codes

There is a very systematic method for creating the logical zero state using the stabilizer generator matrix [1]. The method is a three part process.

  1. Bring the generator matrix to standard form.
  2. Construct logical operators of the code.
  3. Use Gottesman's algorithm to determine the sequence of gates in the encoding circuit.

We are going to demonstrate the method using this matrix. I will do some of the work using a small python library I am writing to play with quantum stabilizer codes. It's called stac.

Let's load up the Steane code.

# I will need some of these helper libraries
import stac
from qiskit import QuantumCircuit, execute, Aer
from tabulate import tabulate
cd = stac.CommonCodes.generate_code('[[7,1,3]]')
stac.print_matrix(cd.gens_mat, augmented=True)

(111100000000001100110000000010101010000000000000011110000000000110011000000001010101)\displaystyle \left(\begin{array}{ccccccc|ccccccc} 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 1 & 0 & 0 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 1 & 0 & 1 & 0 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 1 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 0 & 0 & 1 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 & 0 & 1 & 0 & 1 \\ \end{array}\right)

1. Bring the generator matrix to standard form

The matrix above is the generator matrix HH of the Steane code that we saw yesterday. It has m=6m=6 generators/row, and 2n=142n=14 columns, so n=7n=7 physical qubits. And it encodes k=nm=1k=n-m=1 logical qubit.

We will now transform it to the standard form. What does this mean? Note that each row of HH represents a generator of the stabilizer group associated with the Steane code. We can manipulate the generators and qubits in the following way.

  • We know from group theory that we can replace one of the generators gig_i with gigjg_ig_j where gjg_j is another generator, and we will have obtained a new generating set for the same group. Multiplying generators corresponds to adding rows of HH (why?).
  • We can also reindex the generators, which corresponds to permuting the rows HH.
  • We can also reindex the physical qubits, which corresponds to simultaneously permuting the columns on both sides of HH.

All of these operations on the generators don't change the code, only create a new representation of it. The corresponding operations on HH are simply those that we do when doing Gaussian reduction on a matrix. Recall that any matrix can brought into a reduced row echelon form (RREF), which is unique. We will use this fact to create the standard from of HH. The goal is to simplify the generators so they have as few Paulis in them as possible.

The standard form is obtained as follows. First bring the XX part of HH to RREF, remembering to permute columns on both sides. This reduces HH to H=(IABC00DE).H = \left(\begin{array}{cc|cc} I & A & B & C \\ 0 & 0 & D & E \end{array}\right). Let rr be the rank of the XX part of HH. Then, the blocks have

  • rr and mrm-r rows respectively,
  • rr and nrn-r columns respectively.

If we then bring EE to RREF, due to the properties of the code, HH will reduce to H=(IA1A2BC1C2000DIE2).H = \left(\begin{array}{ccc|ccc} I & A_1 & A_2 & B & C_1 & C_2 \\ 0 & 0 & 0 & D & I & E_2 \end{array}\right). Here, the columns of the blocks are of size rr, mrm-r and kk respectively.

This is the standard form of HH. For the Steane code, it is as follows.

cd.standard_form()
stac.print_matrix(cd.standard_gens_mat, augmented=True)

(100101100000000101101000000000111100000000000000011110000000000101010100000000110011)\displaystyle \left(\begin{array}{ccccccc|ccccccc} 1 & 0 & 0 & 1 & 0 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 1 & 1 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 1 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 1 & 0 & 1 & 0 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 0 & 0 & 1 & 1 \\ \end{array}\right)

2. Construct logical operators of the code

Yesterday, I presented two operators and called them the logical Xˉ\bar{X} and Zˉ\bar{Z} operators. In fact, there are many different operators that can equivalently act as the logical Xˉ\bar{X} and Zˉ\bar{Z}. Here, we use Gottesman's method to construct them. The claim is that [2], the logical Xˉ\bar{X}s are the rows of the matrix (0E2TIE2T(C1T+C2T)00).(0 E_2^T I | E_2^T(C_1^T + C_2^T) 0 0). Remember that some codes encode more than one logical qubit, so if a code encodes kk logical qubit, we will need kk logical Xˉ\bar{X}s.

Similarly, the logical Zˉ\bar{Z}s are (000A2T0I).(0 0 0 | A_2^T 0 I).

We can read off these from HH, and for the Steane code, they are as follows.

cd.construct_logical_operators()
print("Logical X =", cd.logical_xs)
print("Logical Z =", cd.logical_zs)
Logical X = [[0 0 0 0 1 1 1 0 0 0 0 0 0 0]]
Logical Z = [[0 0 0 0 0 0 0 1 1 0 0 0 0 1]]

3. Use Gottesman's algorithm to determine the sequence of gates in the encoding circuit

Now we are ready to construct the encoding circuit for the zero state. It's a very simple algorithm. Let qubits be numbered 0-(n-1), let qubits 0-(n-k-1) be the ancilla qubits in state 0\ket{0} and the remaining kk qubits be where the unknown state ψ\ket{\psi} is fed in. Here, we will let ψ=ket0\ket{\psi} = ket{0}.

encoding_circuit = []
for i in range(k):
for j in range(r, n-k):
if logical_xs[i, j]:
encoding_circuit.append(["cx", n-k+i, j])

for i in range(r):
encoding_circuit.append(["h", i])
for j in range(n):
if i == j:
continue
if standard_gens_x[i, j] and standard_gens_z[i, j]:
encoding_circuit.append(["cx", i, j])
encoding_circuit.append(["cz", i, j])
elif standard_gens_x[i, j]:
encoding_circuit.append(["cx", i, j])
elif standard_gens_z[i, j]:
encoding_circuit.append(["cz", i, j])

One subtlety here is that instead of using the complex YY gate, we instead let Y=ZXY=ZX, which is a real matrix. So, the place where we have to apply CYCY, we apply CXCX followed by CZCZ.

Executing this algorithm, we get the circuit as follows.

enc_circ = cd.construct_encoding_circuit()
stac.draw_circuit(enc_circ, output='latex')
# unfortunately, qiskit has a nasty habit of rearranging the gates.

code output

We should check that this produces the right state. We should produce the state 0ˉ=0000000+1100110+1111000+0011110\ket{\bar 0} = \ket{0000000} + \ket{1100110}+ \ket{1111000} + \ket{0011110} +1010101+0110011+0101101+1001011.+ \ket{1010101} + \ket{0110011}+ \ket{0101101} + \ket{1001011}.

We will use Qiskit to simulate to encoding circuit.

# I personally like displaying the output state in a nice way.
def simulate_circuit(circuit, n, head=None):
if head is None:
head = ['basis', 'amplitude']
tab = [[bin(i)[2:][-1::-1].ljust(n,'0')] for i in range(2**n)]

qc = QuantumCircuit.from_qasm_str(stac.circuit_to_qasm(circuit))
job = execute(qc, Aer.get_backend('statevector_simulator'),
shots=1,
optimization_level=0)
sv = job.result().get_statevector()

amps = sv.data.real
for i in range(2**n):
tab[i].append(np.round(amps[i],3))

smalltab = []
for t in tab:
if any(t[1:]):
smalltab.append([' ' if val == 0 else val for val in t])

print(tabulate(smalltab, headers=head))

simulate_circuit(enc_circ, cd.num_physical_qubits)
  basis    amplitude
-------  -----------
0000000        0.354
1111000        0.354
1100110        0.354
0011110        0.354
1010101        0.354
0101101        0.354
0110011        0.354
1001011        0.354

We can similarly, create the logical one state. There are two ways go about this. Either,

  1. We feed in the 1\ket{1} state into the circuit, or
  2. We use the logical Xˉ\bar{X} to flip the logical zero state.

Let's do both.

# Option 1
enc_circ_one = [['x', 6]] + enc_circ
stac.draw_circuit(enc_circ_one, output='latex')
simulate_circuit(enc_circ_one, cd.num_physical_qubits)
# looks good

code output

  basis    amplitude
-------  -----------
0110100        0.354
1001100        0.354
1010010        0.354
0101010        0.354
1100001        0.354
0011001        0.354
0000111        0.354
1111111        0.354
# Option 2. Use the logical X created above
enc_circ_flip = enc_circ + [['x', 4], ['x', 5], ['x', 6]]
stac.draw_circuit(enc_circ_flip, output='latex')
# anybody know how to fix qiskit's draw to respect the input ordering?
simulate_circuit(enc_circ_flip, cd.num_physical_qubits)
# looks good

code output

  basis    amplitude
-------  -----------
0110100        0.354
1001100        0.354
1010010        0.354
0101010        0.354
1100001        0.354
0011001        0.354
0000111        0.354
1111111        0.354

Addendum

Actually, there is minor 'mistake' in Gottesman's encoding circuit algorithm. For some codes, if you feed in a basis states at the input, it's possible the created logical basis state is a 1-1 eigenvectors of some of the stabilizer generators. This is not a real problem. On a real quantum computer, for real applications, we are unlikely to initialize our circuit in anything but the logical zero state. Instead, we create the states of interest by then manipulating the logical zero state using logical operations. However, there might be quantum information protocols where such unknown quantum states need to be encoded, where this fact is relevant.

In any case, we can fix this problem, by using the concept of destabilizers. But I will defer this discussion to a later time.

[1] D. Gottesman, Stabilizer Codes and Quantum Error Correction, arXiv:quant-ph/9705052.

[2] Gaitan, Frank, Quantum Error Correction and Fault Tolerant Quantum Computing (CRC Press, 2008).