Skip to content

Commit

Permalink
Merge pull request #127 from Infleqtion/logical-ops
Browse files Browse the repository at this point in the history
Change logical operator format
  • Loading branch information
perlinm authored Aug 9, 2024
2 parents 1d1b80a + 775339e commit 16e9e5e
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 28 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ filterwarnings = [

[tool.ruff]
line-length = 100
lint.extend-select = ["I"]

[tool.black]
color = true
Expand Down
41 changes: 27 additions & 14 deletions qldpc/codes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ class QuditCode(AbstractCode):
"""

_matrix: galois.FieldArray
_full_logical_ops: galois.FieldArray | None = None
_logical_ops: galois.FieldArray | None = None

def __init__(
self,
Expand Down Expand Up @@ -659,7 +659,7 @@ def conjugate(
matrix[:, :, qudits] = np.roll(matrix[:, :, qudits], 1, axis=1)
return matrix.reshape(num_checks, -1)

def get_logical_ops(self) -> galois.FieldArray:
def get_logical_ops(self, pauli: PauliXZ | None = None) -> galois.FieldArray:
"""Complete basis of nontrivial logical operators for this code.
Logical operators are represented by a three-dimensional array `logical_ops` with dimensions
Expand All @@ -678,12 +678,22 @@ def get_logical_ops(self) -> galois.FieldArray:
logical operator for logical qudit `r` addresses physical qudit `j` with a physical X-type
(Z-type) operator.
If passed a pauli operator (Pauli.X or Pauli.Z), return the two-dimensional array of logical
operators of that type.
Logical operators are constructed using the method described in Section 4.1 of Gottesman's
thesis (arXiv:9705052), slightly modified for qudits.
"""
# memoize manually because other methods may modify the logical operators computed here
if self._full_logical_ops is not None:
return self._full_logical_ops
assert pauli is None or pauli in PAULIS_XZ

# if requested, retrieve logical operators of one type only
if pauli is not None:
return self.get_logical_ops()[pauli]

# memoize manually because other methods may modify the logical operators computed here
if self._logical_ops is not None:
return self._logical_ops

num_qudits = self.num_qudits
dimension = self.dimension
Expand Down Expand Up @@ -750,8 +760,9 @@ def get_logical_ops(self) -> galois.FieldArray:
# reshape and return
logicals_x = logicals_x.reshape(dimension, 2 * num_qudits)
logicals_z = logicals_z.reshape(dimension, 2 * num_qudits)
self._full_logical_ops = self.field(np.stack([logicals_x, logicals_z]))
return self._full_logical_ops
shape = (2, self.dimension, 2 * self.num_qudits)
self._logical_ops = self.field(np.stack([logicals_x, logicals_z]).reshape(shape))
return self._logical_ops


class CSSCode(QuditCode):
Expand All @@ -775,7 +786,6 @@ class CSSCode(QuditCode):
code_z: ClassicalCode # Z-type parity checks, measuring X-type errors

_conjugated: slice | Sequence[int]
_logical_ops: galois.FieldArray | None = None
_exact_distance_x: int | float | None = None
_exact_distance_z: int | float | None = None
_balanced_codes: bool
Expand Down Expand Up @@ -1100,9 +1110,8 @@ def get_logical_ops(self, pauli: PauliXZ | None = None) -> galois.FieldArray:
(Z-type) operator. The fact that logical operators come in conjugate pairs means that
`logical_ops(Pauli.X)[r, :] @ logical_ops(Pauli.Z)[s, :] == int(r == s)`.
If passed a pauli operator (Pauli.X or Pauli.Z), return the two-dimensional array with
dimensions `(k, n)`, in which `logical_ops[r, :]` indicates the support of the purely-X-type
or purely-Z-type logical operator on qudit `r`.
If passed a pauli operator (Pauli.X or Pauli.Z), return the two-dimensional array of logical
operators of that type.
Logical operators are constructed using the method described in Section 4.1 of Gottesman's
thesis (arXiv:9705052), slightly modified for qudits.
Expand All @@ -1111,8 +1120,7 @@ def get_logical_ops(self, pauli: PauliXZ | None = None) -> galois.FieldArray:

# if requested, retrieve logical operators of one type only
if pauli is not None:
shape = (self.dimension, 2, self.num_qudits)
return self.get_logical_ops()[pauli].reshape(shape)[:, pauli, :]
return self.get_logical_ops()[pauli]

# memoize manually because other methods may modify the logical operators computed here
if self._logical_ops is not None:
Expand Down Expand Up @@ -1211,8 +1219,13 @@ def reduce_logical_op(self, pauli: PauliXZ, logical_index: int, **decoder_args:
assert 0 <= logical_index < self.dimension

# effective check matrix = syndromes and other logical operators
code = self.code_z if pauli == Pauli.X else self.code_x
all_dual_ops = self.get_logical_ops(~pauli) # type:ignore[arg-type]
if pauli == Pauli.X:
code = self.code_z
nonzero_dual_section = slice(self.num_qudits, 2 * self.num_qudits)
else:
code = self.code_x
nonzero_dual_section = slice(self.num_qudits)
all_dual_ops = self.get_logical_ops()[~pauli, :, nonzero_dual_section]
effective_check_matrix = np.vstack([code.matrix, all_dual_ops]).view(np.ndarray)
dual_op_index = code.num_checks + logical_index

Expand Down
29 changes: 15 additions & 14 deletions qldpc/codes/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ def test_qubit_code(num_qubits: int = 5, num_checks: int = 3) -> None:

def test_qudit_code() -> None:
"""Miscellaneous qudit code tests and coverage."""
assert codes.FiveQubitCode().dimension == 1
code = codes.FiveQubitCode()
assert code.dimension == 1
assert code.get_logical_ops(Pauli.X).shape == code.get_logical_ops(Pauli.Z).shape


@pytest.mark.parametrize("field", [2, 3])
Expand Down Expand Up @@ -209,7 +211,7 @@ def test_qudit_ops() -> None:
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 code.get_logical_ops() is code._full_logical_ops
assert code.get_logical_ops() is code._logical_ops

code = codes.QuditCode.from_stabilizers(*code.get_stabilizers(), "I I I I I")
assert np.array_equal(logical_ops, code.get_logical_ops())
Expand Down Expand Up @@ -247,20 +249,19 @@ def test_CSS_ops() -> None:
code.get_random_logical_op(Pauli.X, ensure_nontrivial=False)
code.get_random_logical_op(Pauli.X, ensure_nontrivial=True)

# test that logical operators are dual to each other and have trivial syndromes
logicals_x, logicals_z = code.get_logical_ops(Pauli.X), code.get_logical_ops(Pauli.Z)
assert np.array_equal(logicals_x @ logicals_z.T, np.eye(code.dimension, dtype=int))
assert not np.any(code.matrix_z @ logicals_x.T)
assert not np.any(code.matrix_x @ logicals_z.T)
# test that logical operators have trivial syndromes
logicals_x = code.get_logical_ops(Pauli.X)
logicals_z = code.get_logical_ops(Pauli.Z)
assert not np.any(logicals_x[:, code.num_qudits :])
assert not np.any(logicals_z[:, : code.num_qudits])
assert not np.any(code.matrix @ logicals_x.T)
assert not np.any(code.matrix @ logicals_z.T)
assert code.get_logical_ops() is code._logical_ops

# verify consistency with QuditCode.get_logical_ops
full_logicals = codes.QuditCode.get_logical_ops(code)
full_logicals_x, full_logicals_z = full_logicals[0], full_logicals[1]
assert np.array_equal(full_logicals_x, np.hstack([logicals_x, np.zeros_like(logicals_x)]))
assert np.array_equal(full_logicals_z, np.hstack([np.zeros_like(logicals_x), logicals_z]))
assert not np.any(code.matrix @ full_logicals_x.T)
assert not np.any(code.matrix @ full_logicals_z.T)
# test that logical operators are dual to each other
logicals_x = logicals_x[:, : code.num_qudits]
logicals_z = logicals_z[:, code.num_qudits :]
assert np.array_equal(logicals_x @ logicals_z.T, np.eye(code.dimension, dtype=int))

# successfullly construct and reduce logical operators in a code with "over-complete" checks
dist = 4
Expand Down

0 comments on commit 16e9e5e

Please sign in to comment.