Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Define all Pauli strings (including rows of parity check matrices) in [X|Z] format #196

Merged
merged 10 commits into from
Jan 18, 2025
32 changes: 16 additions & 16 deletions qldpc/circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ def get_encoding_tableau(code: codes.QuditCode) -> stim.Circuit:
# identify stabilizers
matrix = codes.ClassicalCode(code.matrix).canonicalized().matrix
pivots = [int(np.argmax(row != 0)) for row in matrix if np.any(row)]
stabilizers = [op_to_string(row, flip_xz=True) for row in matrix]
stabilizers = [op_to_string(row) for row in matrix]

# construct destabilizers
destabilizers: list[stim.PauliString] = []
for pivot in pivots:
# construct a candidate destabilizer that only anti-commutes with one stabilizer
vector_zx = code.field.Zeros(2 * len(code))
vector_zx[(pivot + len(code)) % (2 * len(code))] = 1
candidate_destabilizer = op_to_string(vector_zx, flip_xz=True)
vector = code.field.Zeros(2 * len(code))
vector[(pivot + len(code)) % (2 * len(code))] = 1
candidate_destabilizer = op_to_string(vector)

# enforce that the candidate destabilizer commutes with all logical operators
for log_x, log_z in zip(logicals_x, logicals_z):
Expand Down Expand Up @@ -204,20 +204,20 @@ def get_transversal_automorphism_group(
logical Pauli group for the deformed QuditCode.
"""
effective_stabilizers = code.matrix if not deform_code else conjugate_xz(code.get_logical_ops())
matrix_z = effective_stabilizers.reshape(-1, 2, len(code))[:, 0, :]
matrix_x = effective_stabilizers.reshape(-1, 2, len(code))[:, 1, :]
matrix_x = effective_stabilizers.reshape(-1, 2, len(code))[:, 0, :]
matrix_z = effective_stabilizers.reshape(-1, 2, len(code))[:, 1, :]
if not local_gates or local_gates == {"H"}:
# swapping sectors = swapping Z <--> X
matrix = np.hstack([matrix_z, matrix_x])
# swapping sectors = swapping X <--> Z
matrix = np.hstack([matrix_x, matrix_z])
elif local_gates == {"S"}:
# swapping sectors = swapping X <--> Y
matrix = np.hstack([matrix_z, matrix_z + matrix_x])
matrix = np.hstack([matrix_z, matrix_x + matrix_z])
elif local_gates == {"SQRT_X"}:
# swapping sectors = swapping Y <--> Z
matrix = np.hstack([matrix_z + matrix_x, matrix_x])
matrix = np.hstack([matrix_x, matrix_x + matrix_z])
else:
# we have a complete local Clifford gate set that can arbitrarily permute Pauli ops
matrix = np.hstack([matrix_z, matrix_x, matrix_z + matrix_x])
matrix = np.hstack([matrix_x, matrix_z, matrix_x + matrix_z])

# compute the automorphism group of an instrumental classical code
instrumental_code = codes.ClassicalCode(matrix)
Expand Down Expand Up @@ -336,15 +336,15 @@ def _get_pauli_permutation_circuit(
for qubit in range(len(code)):
pauli_perm = [automorphism(qubit + ss * len(code)) // len(code) for ss in range(3)]
match pauli_perm:
case [1, 0, 2]: # Z <--> X
case [1, 0, 2]: # X <--> Z
gate_targets["H"].append(qubit)
case [2, 1, 0]: # X <--> Y
case [0, 2, 1]: # X <--> Y
gate_targets["S"].append(qubit)
case [0, 2, 1]: # Y <--> Z
case [2, 1, 0]: # Y <--> Z
gate_targets["H_YZ"].append(qubit)
case [1, 2, 0]: # ZXY <--> XYZ
case [2, 0, 1]: # ZXY <--> XYZ
gate_targets["C_ZYX"].append(qubit) # pragma: no cover
case [2, 0, 1]: # ZXY <--> ZYX
case [1, 2, 0]: # ZXY <--> ZYX
gate_targets["C_XYZ"].append(qubit) # pragma: no cover

for gate, targets in gate_targets.items():
Expand Down
2 changes: 1 addition & 1 deletion qldpc/circuits_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_state_prep() -> None:

# the state of the simulator is a +1 eigenstate of code stabilizers
for row in code.matrix:
string = circuits.op_to_string(row, flip_xz=True)
string = circuits.op_to_string(row)
assert simulator.peek_observable_expectation(string) == 1

# the state of the simulator is a +1 eigenstate of all logical Z operators
Expand Down
170 changes: 72 additions & 98 deletions qldpc/codes/common.py

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions qldpc/codes/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@ def test_qudit_code() -> None:
[0, 1, 0, 0, 1, 0, 0, 1, 1, 0],
[1, 0, 1, 0, 0, 0, 0, 0, 1, 1],
[0, 1, 0, 1, 0, 1, 0, 0, 0, 1],
],
flip_xz=True,
]
)
assert np.array_equal(code.matrix, equiv_code.matrix)

Expand Down Expand Up @@ -332,8 +331,8 @@ def test_qudit_ops() -> None:
code = codes.FiveQubitCode()
logical_ops = code.get_logical_ops()
assert logical_ops.shape == (2 * code.dimension, 2 * code.num_qudits)
assert np.array_equal(logical_ops[0], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0])
assert np.array_equal(logical_ops[1], [0, 1, 1, 0, 0, 0, 0, 0, 0, 1])
assert np.array_equal(logical_ops[0], [0, 0, 0, 0, 1, 1, 0, 0, 1, 0])
assert np.array_equal(logical_ops[1], [0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
assert code.get_logical_ops() is code._logical_ops

code = codes.QuditCode.from_stabilizers(*code.get_stabilizers(), "I I I I I", field=2)
Expand Down
41 changes: 13 additions & 28 deletions qldpc/codes/quantum.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,8 @@ class BBCode(QCCode):

The polynomials A and B induce a "canonical" layout of the data and check qubits of a BBCode.
In the canonical layout, qubits are organized into plaquettes of four qubits that look like
Z L
R X
X L
R Z
where L and R are data qubits, and X and Z are check qubits. More specifically:
- L and R data qubits are addressed by the left and right halves of matrix_x (or matrix_z).
- X are check qubits measure X-type parity checks, and are associated with rows of matrix_x.
Expand Down Expand Up @@ -353,8 +353,8 @@ def get_qubit_pos(
sector, aa, bb = qubit
assert sector in ["L", "R", "X", "Z"]

xx = 2 * aa + int(sector in ["R", "X"])
yy = 2 * bb + int(sector in ["L", "X"])
xx = 2 * aa + int(sector in ["R", "Z"])
yy = 2 * bb + int(sector in ["L", "Z"])
if folded_layout:
xx = 2 * xx if xx < self.orders[0] else (2 * self.orders[0] - 1 - xx) * 2 + 1
yy = 2 * yy if yy < self.orders[1] else (2 * self.orders[1] - 1 - yy) * 2 + 1
Expand Down Expand Up @@ -618,7 +618,7 @@ def get_graph_product(cls, graph_a: nx.DiGraph, graph_b: nx.DiGraph) -> nx.DiGra
node_qudit, sector_qudit = node_snd, sector_snd

# start with an X-type operator
op = QuditOperator((data.get("val", 1), 0))
op = QuditOperator((data.get("val", 0), 0))

# switch to Z-type operator for check qudits in the (0, 1) sector
if sector_check == (0, 1):
Expand Down Expand Up @@ -655,31 +655,16 @@ def get_product_node_map(
cls, nodes_a: Collection[Node], nodes_b: Collection[Node]
) -> dict[tuple[Node, Node], Node]:
"""Map (dictionary) that re-labels nodes in the hypergraph product of two codes."""
num_qudits_a = sum(node.is_data for node in nodes_a)
num_qudits_b = sum(node.is_data for node in nodes_b)
num_checks_a = len(nodes_a) - num_qudits_a
num_checks_b = len(nodes_b) - num_qudits_b
sector_shapes = [
[(num_qudits_a, num_qudits_b), (num_qudits_a, num_checks_b)],
[(num_checks_a, num_qudits_b), (num_checks_a, num_checks_b)],
]

node_map = {}
index_data, index_check = 0, 0
for node_a, node_b in itertools.product(sorted(nodes_a), sorted(nodes_b)):
# identify sector and whether this is a data vs. check qudit
sector = cls.get_sector(node_a, node_b)
is_data = sector in [(0, 0), (1, 1)]

# identify node index
sector_shape = sector_shapes[sector[0]][sector[1]]
index = int(np.ravel_multi_index((node_a.index, node_b.index), sector_shape))
if sector == (1, 1):
index += num_qudits_a * num_qudits_b
if sector == (0, 1):
index += num_checks_a * num_qudits_b

node_map[node_a, node_b] = Node(index=index, is_data=is_data)

if cls.get_sector(node_a, node_b) in [(0, 0), (1, 1)]:
node = Node(index=index_data, is_data=True)
index_data += 1
else:
node = Node(index=index_check, is_data=False)
index_check += 1
node_map[node_a, node_b] = node
return node_map


Expand Down
4 changes: 2 additions & 2 deletions qldpc/codes/quantum_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ def test_twisted_XZZX(width: int = 3) -> None:
zero_4 = np.zeros((mat_2.shape[0],) * 2, dtype=int)
matrix = np.block(
[
[zero_3, mat_2.T, mat_1, zero_2],
[-mat_2, zero_4, zero_1, mat_1.T],
[zero_1, mat_1.T, -mat_2, zero_4],
[mat_1, zero_2, zero_3, mat_2.T],
]
)

Expand Down
20 changes: 10 additions & 10 deletions qldpc/external/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,16 @@ def maybe_get_webpage(order: int) -> str | None:
[(0, 3, 1, 4, 9, 5), (2, 8, 6)],
[(0, 6, 8), (1, 9, 2), (3, 5, 7)],
],
"AutomorphismGroup(CheckMatCode([[1,0,0,0,1,0,1,0,1,0,1,1,0,1,1],[0,1,0,0,1,0,1,1,1,1,0,0,1,1,0],[0,0,1,0,1,1,1,1,0,1,1,1,0,0,0],[0,0,0,1,1,1,0,1,0,0,1,0,1,1,1]],GF(2)))": [
[(0, 1), (7, 10), (9, 11), (12, 14)],
[(2, 3), (6, 13), (9, 12), (11, 14)],
[(1, 2), (5, 12), (8, 11), (10, 13)],
[(1, 8), (4, 7), (6, 9), (12, 13)],
[(3, 5), (4, 13), (7, 12), (10, 14)],
[(2, 9), (3, 12), (6, 11), (13, 14)],
[(2, 6), (3, 12), (4, 7), (5, 10), (9, 11), (13, 14)],
[(3, 12), (4, 10), (5, 7), (13, 14)],
[(3, 14), (4, 7), (5, 10), (12, 13)],
"AutomorphismGroup(CheckMatCode([[1,0,0,0,1,1,1,0,1,1,0,1,0,1,0],[0,1,0,0,1,0,0,1,1,0,0,1,1,1,1],[0,0,1,0,1,1,1,0,0,0,1,1,1,0,1],[0,0,0,1,1,1,0,1,1,1,1,0,1,0,0]],GF(2)))": [
[(0, 1), (5, 12), (6, 14), (7, 9)],
[(2, 3), (6, 9), (7, 14), (8, 11)],
[(1, 2), (5, 8), (6, 13), (7, 10)],
[(1, 13), (4, 12), (7, 8), (11, 14)],
[(3, 10), (4, 8), (5, 9), (7, 12)],
[(2, 6), (4, 12), (5, 10), (11, 14)],
[(2, 11), (3, 8), (6, 14), (7, 9)],
[(3, 8), (4, 10), (5, 12), (7, 9)],
[(3, 9), (4, 12), (5, 10), (7, 8)],
],
"AutomorphismGroup(CheckMatCode([[1,1,1,1,0,0,0,0,1,1,1,1],[0,0,0,0,1,1,1,1,1,1,1,1]],GF(2)))": [
[(4, 9), (5, 8, 6, 11), (7, 10)],
Expand Down
8 changes: 3 additions & 5 deletions qldpc/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,11 @@ def conjugate_xz(strings: npt.NDArray[np.int_]) -> npt.NDArray[np.int_]:
return strings.reshape(-1, 2, strings.shape[-1] // 2)[:, ::-1, :].reshape(strings.shape)


def op_to_string(op: npt.NDArray[np.int_], flip_xz: bool = False) -> stim.PauliString:
def op_to_string(op: npt.NDArray[np.int_]) -> stim.PauliString:
"""Convert an integer array that represents a Pauli string into a stim.PauliString.

The (first, second) half the array indicates the support of (X, Z) Paulis, unless flip_xz==True.
The (first, second) half the array indicates the support of (X, Z) Paulis.
"""
op = conjugate_xz(op) if flip_xz else op
support_xz = np.array(op, dtype=int).reshape(2, -1)
paulis = [Pauli((support_xz[0, qq], support_xz[1, qq])) for qq in range(support_xz.shape[1])]
return stim.PauliString(map(str, paulis))
Expand All @@ -59,8 +58,7 @@ def op_to_string(op: npt.NDArray[np.int_], flip_xz: bool = False) -> stim.PauliS
for qubit in range(num_qubits):
val_x = int(op[qubit])
val_z = int(op[qubit + num_qubits])
pauli = Pauli((val_x, val_z))
paulis += str(pauli if not flip_xz else ~pauli)
paulis = str(Pauli((val_x, val_z)))
return stim.PauliString(paulis)


Expand Down
2 changes: 1 addition & 1 deletion qldpc/objects_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_pauli_strings() -> None:
"""Stabilizers correctly converted into stim.PauliString objects."""
code = codes.FiveQubitCode()
assert all(
objects.op_to_string(row, flip_xz=True) == stim.PauliString(stabilizer.replace(" ", ""))
objects.op_to_string(row) == stim.PauliString(stabilizer.replace(" ", ""))
for row, stabilizer in zip(code.matrix, code.get_stabilizers())
)

Expand Down
Loading