Verifying Logical Clifford Gadgets¶
Motivation: Logical Cliffords can have non-obvious implementations, especially for \(k>1\) codes. We want to be able to validate implementations and catch bugs in our logical gadgets.
For example, see Table 2 from Simple logical quantum computation with concatenated symplectic double codes. Below is a section of this table that shows the physical implementation and logical action for
The
guppyft.verifymodule checks logical Clifford semantics against physical implementation, using thevalid_clifford_implementationfunction.The tool works for \(k>1\) codes and also operations between two code blocks.
This approach is fairly general and works for any stabilizer code. Includes non-CSS codes.
We will specify the logical semantics with a Guppy function which acts on an array of \(k\) qubits. Let’s see an example for the \([[7, 1, 3]]\) Steane code. The action of a logical Hadamard in the Steane code is as follows…
from guppylang import guppy
from guppylang.std.builtins import array
from guppylang.std.quantum import qubit, h
@guppy
def steane_specify_h(qs: array[qubit, 1]) -> None:
h(qs[0])
We can provide the implementation of the logical operation as a function which acts on an array of \(n\) physical qubits.
@guppy
def steane_impl_h(block: array[qubit, 7]) -> None:
for i in range(len(block)):
h(block[i])
The verifier then calculates the Clifford tableau of both of them, and checks that they are equivalent. Since the logical specification is in terms of \(k\) logical qubits and the implementation is in terms of \(n\) physical qubits, we need to expand the tableau of \(k\) logical qubits to \(n\) physical, using the definition of the stabilizer code. The definition of the stabilizer code tells us how to replace a logical Pauli with its physical Pauli string realization, as well as which additional Pauli strings we need to introduce to account for the code’s stabilizer group.
Providing the stabilizer code definition¶
Before we can get to verifying logical Cliffords, we need a way to specify the key properties of a stabilizer code. We provide a StabilizerCode dataclass which specifies the \([[n, k, d]]\) code parameters as integers as well as the stabilizer generators and logical operators.
@dataclass(frozen=True)
class StabilizerCode:
num_physical_qubits: int
num_logical_qubits: int
distance: int
generators: pauli.SignTermSet
x_logicals: pauli.SignTerms
z_logicals: pauli.SignTerms
The stabilizer generators and logical operators are represented using types from Zixy, a library which provides memory efficient Pauli operator implementations in Rust.
A StabilizerCode definition can be given by specifying the fields directly. However its usually easier to use the from_python_strings utility method.
Let’s provide definitions for the Steane code.
from guppyft.code_def import StabilizerCode
STEANE_DEF = StabilizerCode.from_python_strings(
num_physical_qubits=7,
num_logical_qubits=1,
distance=3,
generators=["XXXXIII", "IXXIXXI", "IIXXIXX", "ZZZZIII", "IZZIZZI", "IIZZIZZ"],
x_logicals=["XXXXXXX"],
z_logicals=["ZZZZZZZ"],
)
We can also provide a code definition for the non-CSS \([[4, 2, 2]]\) code which is discussed in the paper entitled “Simple logical quantum computation with concatenated symplectic double codes” (N. Berthusen and E. Durso-Sabina - https://arxiv.org/pdf/2510.18753)
With non-CSS codes, each stabilizer generator can be made ofa mixture of \(X\) and \(Z\) operators. The same is true of the logical operators.
NON_CSS_4_2_2 = StabilizerCode.from_python_strings(
num_physical_qubits=4,
num_logical_qubits=2,
distance=2,
generators=["XZZX", "ZXXZ"],
x_logicals=["ZIXI", "IZIX"],
z_logicals=["IZZI", "ZIIZ"],
)
There is some basic __post_init__ validation to check we are providing reasonable StabilizerCode definitions.
For example, all of the stabilizer generators must commute with one another. If they do not mutually commute, then the code definition is rejected.
Verifying the implementation of logical Cliffords¶
Now we can get on to verifying logical operations. We’ll start by verifying some operations functions in the Steane code. This is done with the valid_clifford_implementation function.
from guppyft.verify import valid_clifford_implementation
Basic examples for the Steane code¶
First, let’s check the logical Hadamard implementation we defined above.
valid_clifford_implementation(
steane_specify_h, steane_impl_h, STEANE_DEF
) # True => implementation is valid
True
We can also verify operators between two code blocks. As an example, let’s verify the implementation of the transversal CX in the Steane code.
from guppylang.std.quantum import cx
@guppy
def steane_specify_cx(
first_block: array[qubit, 1], second_block: array[qubit, 1]
) -> None:
cx(first_block[0], second_block[0])
@guppy
def steane_impl_cx(first_block: array[qubit, 7], second_block: array[qubit, 7]) -> None:
for i in range(len(first_block)):
cx(first_block[i], second_block[i])
valid_clifford_implementation(
steane_specify_cx, steane_impl_cx, STEANE_DEF
) # Transversal CX impl is valid.
True
We can also detect invalid implementations. If we try to implement a logical \(S\) gate in the Steane code by applying physical \(S\) across across all seven physical qubits, we get False indicating that our implementation of logical \(S\) is wrong.
from guppylang.std.quantum import s
@guppy
def steane_specify_s(block: array[qubit, 1]) -> None:
s(block[0])
@guppy
def steane_impl_s_incorrect(block: array[qubit, 7]) -> None:
for i in range(len(block)):
s(block[i])
valid_clifford_implementation(
steane_specify_s, steane_impl_s_incorrect, STEANE_DEF
) # Invalid!
False
It turns out the correct way to implement logical \(S\) in the Steane code is by applying \(S^\dagger\) across the physical qubits.
from guppylang.std.quantum import sdg
@guppy
def steane_impl_s_corrected(block: array[qubit, 7]) -> None:
for i in range(len(block)):
sdg(block[i])
valid_clifford_implementation(
steane_specify_s, steane_impl_s_corrected, STEANE_DEF
) # Now valid!
True
Examples for \(k>1\) codes¶
So far, the examples with the Steane code have just been simple transversal operations. However, logical Clifford operations for \(k > 1\) codes can also be verified. Let’s consider the \(k = 2\) CSS \([[ 4 , 2 , 2 ]]\) code to see this
CSS_4Q_DEF = StabilizerCode.from_python_strings(
num_physical_qubits=4,
num_logical_qubits=2,
distance=2,
generators=["XXXX", "ZZZZ"],
x_logicals=["XXII", "XIXI"],
z_logicals=["IZIZ", "IIZZ"],
)
In this \([[4, 2, 2]]\) code, we can implement a logical CX gate (intrablock) by simply swapping the physical qubits.
from guppylang.std.mem import mem_swap
@guppy
def specify_intra_block_cx(block: array[qubit, 2]) -> None:
cx(block[0], block[1])
@guppy
def implement_intra_block_cx(block: array[qubit, 4]) -> None:
mem_swap(block[3], block[1])
valid_clifford_implementation(
specify_intra_block_cx, implement_intra_block_cx, CSS_4Q_DEF
)
True
We can also verify the implementation of logical SWAP in the non-CSS \([[4, 2, 2]]\) code. This is implemented by applying the Hadamard gate to all of the physical qubits.
@guppy
def specify_swap_non_css(block: array[qubit, 2]) -> None:
mem_swap(block[0], block[1])
@guppy
def implement_swap_non_css(block: array[qubit, 4]) -> None:
for i in range(len(block)):
h(block[i])
valid_clifford_implementation(
specify_swap_non_css,
implement_swap_non_css,
NON_CSS_4_2_2,
)
True
Verifying preparation of logical Pauli eigenstates¶
The testing framework also allows us to verify state preparation. For example we can check preparation of logical \(|0\rangle\) \(\big(|0\rangle_{L}\big)\) in the \([[4, 2, 2]]\) code as follows.
from guppyft.verify import valid_stabilizer_state_preparation
@guppy
def specify_zero_state() -> array[qubit, 2]:
return array(qubit() for _ in range(2))
@guppy
def implement_non_ft_zero_state() -> array[qubit, 4]:
block = array(qubit() for _ in range(4))
h(block[0])
cx(block[0], block[1])
cx(block[0], block[2])
cx(block[0], block[3])
return block
valid_stabilizer_state_preparation(
specify_zero_state, implement_non_ft_zero_state, CSS_4Q_DEF
)
True
Explaining the verifier step by step¶
Let’s go back to the logical Hadamard in Steane. How do we work out that logical Hadamard can be implemented by broadcasting a Hadamard gate across all seven physical qubits?
Firstly we calculate the stabilizers of a \(2k\) qubit Choi state which encodes the logical Hadamard.
from guppyft.verify._verify import _compute_stabilizers_single_block_unitary
from guppyft.verify._expansion import get_expanded_stabilizer_set
from guppyft.code_def import identity_code
# Get the 2k stabilizers for the 2k qubit Choi state encoding the logical operation.
semantic_choi_stabilizers = _compute_stabilizers_single_block_unitary(
code=identity_code(k=1),
clifford_func=steane_specify_h,
num_selene_qubits=2 * STEANE_DEF.num_logical_qubits,
)
semantic_choi_stabilizers.to_dataframe()
| Component | Coefficient | |
|---|---|---|
| 0 | X0 Z1 | +1 |
| 1 | Z0 X1 | +1 |
Notice how the stabilizers are closely related to the stabilizers of the Bell state \(\{X_0X_1, Z_0Z_1\}\) but with a logical Hadamard applied to \(X_1\) and \(Z_1\).
Next, we expand the \(2k\) logical stabilizers to \(2k\) stabilizers of size \(2n\). We also add the \(2(n-k)\) stabilizer generators of our code. For each code block there are \(n-k\) generators, so \(2\) blocks give us \(2(n-k)\). We have \(2k + 2(n-k) = 2n\) stabilizers in total.
We perform this expansion using the definition of the logical operators for the particular StabilizerCode we are working with. For the Steane code, we do the following expansion:
expanded_semantic_stabilizers = get_expanded_stabilizer_set(
semantic_choi_stabilizers, STEANE_DEF, num_blocks=2
)
expanded_semantic_stabilizers.to_dataframe()
| Component | Coefficient | |
|---|---|---|
| 0 | X0 X1 X2 X3 X4 X5 X6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 | +1 |
| 1 | Z0 Z1 Z2 Z3 Z4 Z5 Z6 X7 X8 X9 X10 X11 X12 X13 | +1 |
| 2 | X0 X1 X2 X3 | +1 |
| 3 | X1 X2 X4 X5 | +1 |
| 4 | X2 X3 X5 X6 | +1 |
| 5 | Z0 Z1 Z2 Z3 | +1 |
| 6 | Z1 Z2 Z4 Z5 | +1 |
| 7 | Z2 Z3 Z5 Z6 | +1 |
| 8 | X7 X8 X9 X10 | +1 |
| 9 | X8 X9 X11 X12 | +1 |
| 10 | X9 X10 X12 X13 | +1 |
| 11 | Z7 Z8 Z9 Z10 | +1 |
| 12 | Z8 Z9 Z11 Z12 | +1 |
| 13 | Z9 Z10 Z12 Z13 | +1 |
In order test two tableaux for equality, we have to convert to a suitable normal form. This is because two tableaux which look different can be equivalent up to multiplication by a Pauli stabilizer. We canonicalize the tableaux using the SignTerms.canonicalize_all() method which uses a process similar to Gaussian elimination.
expanded_semantic_stabilizers.canonicalize_all() # Canonicalize Clifford tableau
expanded_semantic_stabilizers.to_dataframe()
| Component | Coefficient | |
|---|---|---|
| 0 | X0 X3 X6 Z11 Z12 Z13 | +1 |
| 1 | X1 X3 X5 Z11 Z12 Z13 | +1 |
| 2 | X2 X3 X5 X6 | +1 |
| 3 | X4 X5 X6 Z11 Z12 Z13 | +1 |
| 4 | Z4 Z5 Z6 X7 X10 X13 | +1 |
| 5 | Z4 Z5 Z6 X8 X10 X12 | +1 |
| 6 | X9 X10 X12 X13 | +1 |
| 7 | Z4 Z5 Z6 X11 X12 X13 | +1 |
| 8 | Z0 Z3 Z4 Z5 | +1 |
| 9 | Z1 Z3 Z4 Z6 | +1 |
| 10 | Z2 Z3 Z5 Z6 | +1 |
| 11 | Z7 Z10 Z11 Z12 | +1 |
| 12 | Z8 Z10 Z11 Z13 | +1 |
| 13 | Z9 Z10 Z12 Z13 | +1 |
Now we calculate the \(2n\) stabilizers of the Choi state encoding the \(n\) qubit implementation of the logical operation. In this case, the physical Hadamard acts on seven qubits. Therefore we expect our implementation tableau to contain \(2*7 =14\) Pauli stabilizers.
# Calculate the 2n stabilizers of the Choi state encoding the physical operation.
implementation_stabilizers = _compute_stabilizers_single_block_unitary(
STEANE_DEF, steane_impl_h, num_selene_qubits=2 * (STEANE_DEF.num_physical_qubits)
)
implementation_stabilizers.to_dataframe()
| Component | Coefficient | |
|---|---|---|
| 0 | X0 X3 X6 Z11 Z12 Z13 | +1 |
| 1 | Z0 Z3 Z6 X11 X12 X13 | +1 |
| 2 | X1 X3 X5 Z11 Z12 Z13 | +1 |
| 3 | Z1 Z3 Z5 X11 X12 X13 | +1 |
| 4 | X2 X3 X5 X6 | +1 |
| 5 | Z2 Z3 Z5 Z6 | +1 |
| 6 | X4 X5 X6 Z11 Z12 Z13 | +1 |
| 7 | Z4 Z5 Z6 X11 X12 X13 | +1 |
| 8 | X7 X10 X11 X12 | +1 |
| 9 | Z7 Z10 Z11 Z12 | +1 |
| 10 | X8 X10 X11 X13 | +1 |
| 11 | Z8 Z10 Z11 Z13 | +1 |
| 12 | X9 X10 X12 X13 | +1 |
| 13 | Z9 Z10 Z12 Z13 | +1 |
We now canonicalize these implementation stabilizers as we did above for the semantic stabilizers.
implementation_stabilizers.canonicalize_all() # Canonicalize Clifford tableau
implementation_stabilizers.to_dataframe()
| Component | Coefficient | |
|---|---|---|
| 0 | X0 X3 X6 Z11 Z12 Z13 | +1 |
| 1 | X1 X3 X5 Z11 Z12 Z13 | +1 |
| 2 | X2 X3 X5 X6 | +1 |
| 3 | X4 X5 X6 Z11 Z12 Z13 | +1 |
| 4 | Z4 Z5 Z6 X7 X10 X13 | +1 |
| 5 | Z4 Z5 Z6 X8 X10 X12 | +1 |
| 6 | X9 X10 X12 X13 | +1 |
| 7 | Z4 Z5 Z6 X11 X12 X13 | +1 |
| 8 | Z0 Z3 Z4 Z5 | +1 |
| 9 | Z1 Z3 Z4 Z6 | +1 |
| 10 | Z2 Z3 Z5 Z6 | +1 |
| 11 | Z7 Z10 Z11 Z12 | +1 |
| 12 | Z8 Z10 Z11 Z13 | +1 |
| 13 | Z9 Z10 Z12 Z13 | +1 |
Now that we have two Clifford tableaux in a suitable normal form, we can test them for equality.
expanded_semantic_stabilizers == implementation_stabilizers
True
Summary of available features¶
The following features are available for verification of Clifford operations with valid_clifford_implementation
Works for \(k>1\) codes
Works for operations on a single logical block or between two logical blocks
Works for non-CSS codes (e.g. the \([[5, 1, 3]]\) code)
Ancilla qubits can be used in the implementation
The features above are also available in valid_stabilizer_state_preparation which can be used to verify the preparation of logical Pauli eigenstates (e.g. \(|0\rangle_L\), \(|+\rangle_L\))
