Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions deltakit-explorer/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ dependencies = [
"adjustText~=1.3",
"pandas>=2.2.2",
"pandas~=2.3; python_version >= '3.13'",
"ldpc~=2.2",
# >=3.2.3: earlier 3.2 releases import the deprecated ``numpy.core`` in
# ``unumpy``, which fails under the test suite's ``filterwarnings = error``.
"uncertainties>=3.2.3,<4",
Expand Down Expand Up @@ -105,13 +104,6 @@ files = "src/**/*.py"
#Β The below list of disabled checks should eventually be empty.
disable_error_code = "arg-type,assignment,attr-defined,dict-item,index,list-item,misc,operator,override,return-value,union-attr,var-annotated"

# The ldpc.mod2 stubs files contain syntax errors, and so make mypy fail earlier than expected
# with a SyntaxError. Ignoring the whole module for mypy.
[[tool.mypy.overrides]]
module = ["ldpc.mod2"]
follow_imports = "skip"
follow_imports_for_stubs = true

[[tool.mypy.overrides]]
module = ["adjustText"]
follow_untyped_imports = true
Expand Down
56 changes: 43 additions & 13 deletions deltakit-explorer/src/deltakit_explorer/codes/_logicals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@

from collections.abc import Collection, Iterable

import galois
import numpy as np
from deltakit_circuit import PauliX, PauliY, PauliZ, Qubit
from deltakit_circuit._qubit_identifiers import _PauliGate
from deltakit_stim import PauliString, Tableau
from ldpc import mod2
from numpy.typing import NDArray

from deltakit_explorer.codes._css._stabiliser_helper_functions import (
Expand Down Expand Up @@ -241,6 +241,28 @@ def get_logical_operators_from_css_parity_check_matrices(
)


def pivot_rows(parity_check_matrix: galois.FieldArray) -> NDArray[np.int_]:
"""
Compute the pivot row indices of a dense binary check matrix over GF(2).

Args:
parity_check_matrix: A dense binary check matrix represented as a
``galois.FieldArray`` over GF(2).

Returns:
The pivot row indices of ``parity_check_matrix``.
"""
# Pivot rows of ``parity_check_matrix`` are the non-zero pivot columns of
# ``parity_check_matrix.T``.
row_reduced = parity_check_matrix.T.row_reduce()
pivots: list[int] = []
for row in row_reduced:
non_zero_row = np.flatnonzero(row != 0)
if non_zero_row.size:
pivots.append(int(non_zero_row[0]))
return np.asarray(pivots, dtype=np.int_)


def css_code_compute_logicals(
hx: NDArray[np.floating], hz: NDArray[np.floating]
) -> tuple[NDArray[np.floating], NDArray[np.floating]]:
Expand Down Expand Up @@ -277,18 +299,26 @@ def css_code_compute_logicals(
def compute_lz(
_hx: NDArray[np.floating], _hz: NDArray[np.floating]
) -> NDArray[np.floating]:
# lz logical operators
# lz\in ker{hx} AND \notin Im(Hz.T)

# compute the kernel basis of hx
# Note that because inputs are dense arrays, it is fine for every array to be dense in this
# function.
ker_hx = mod2.nullspace(_hx).todense()
# Row reduce to find vectors in kx that are not in the image of hz.T.
log_stack = np.vstack([_hz, ker_hx])

rank_hz = mod2.rank(_hz)
pivots = mod2.pivot_rows(log_stack)[rank_hz:]
"""Compute a basis of logical operators for one side of a CSS code.

This function finds operators that satisfy the checks in ``_hx`` but are
not just combinations of the checks in ``_hz``.

Args:
_hx: Check matrix used to define valid operators.
_hz: Check matrix used to remove redundant operators.

Returns:
A binary matrix whose rows are logical operators.
"""
_hx_gf = galois.GF2(np.asarray(_hx, dtype=np.int_))
Comment thread
rolandriver marked this conversation as resolved.
_hz_gf = galois.GF2(np.asarray(_hz, dtype=np.int_))

ker_hx_gf = _hx_gf.null_space()
rank_hz_gf = np.linalg.matrix_rank(_hz_gf)

log_stack = np.vstack((_hz_gf, ker_hx_gf))
pivots = pivot_rows(log_stack)[rank_hz_gf:]

return np.asarray(log_stack[pivots])

Expand Down
203 changes: 203 additions & 0 deletions deltakit-explorer/tests/unit/codes/test_logicals.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from itertools import product

import deltakit_circuit as circuit
import deltakit_stim as stim
import numpy as np
import numpy.typing as npt
import pytest

from deltakit_explorer.codes._logicals import (
css_code_compute_logicals,
paulistring_to_operator,
)

Expand Down Expand Up @@ -32,3 +37,201 @@ def test_paulistring_to_operator(string):
# operators_ref = [tuple(stim.PauliString(i) for i in j) for j in operators]
# operators_res = get_str_logical_operators_from_tableau(stabilisers)
# operators_res == operators_ref


def _binary_matrix(rows: npt.ArrayLike) -> npt.NDArray[np.uint8]:
"""Convert binary rows into a dense ``uint8`` array.

Args:
rows: Row vectors to place in the matrix.

Returns:
A dense ``uint8`` array built from ``rows``.
"""
return np.asarray(rows, dtype=np.uint8)


def _empty_binary_matrix(n_cols: int) -> npt.NDArray[np.uint8]:
"""Construct an empty dense ``uint8`` array with a fixed column count.

Args:
n_cols: Number of columns in the returned matrix.

Returns:
A dense ``uint8`` array of shape ``(0, n_cols)``.
"""
return np.zeros((0, n_cols), dtype=np.uint8)


def _binary_row_space(matrix: npt.NDArray[np.uint8]) -> set[tuple[int, ...]]:
"""Enumerate the row space of a binary array using GF(2) arithmetic.

Args:
matrix: A dense binary ``uint8`` array.

Returns:
The set of all binary vectors obtainable as mod-2 sums of rows of
``matrix``, represented as tuples.

Notes:
This helper uses a brute-force enumeration over all binary choices of row
coefficients, so it is only suitable for the small matrices used in these
tests.
"""
num_rows, num_cols = matrix.shape

if num_rows == 0:
return {tuple([0] * num_cols)}

vectors: set[tuple[int, ...]] = set()
for coefficients in product((0, 1), repeat=num_rows):
vector = np.zeros(num_cols, dtype=np.uint8)
for coefficient, row in zip(coefficients, matrix, strict=True):
if coefficient:
vector ^= row
vectors.add(tuple(int(value) for value in vector))

return vectors


def _binary_null_space(matrix: npt.NDArray[np.uint8]) -> set[tuple[int, ...]]:
"""Enumerate the null space of a binary array using GF(2) arithmetic.

Args:
matrix: A dense binary ``uint8`` array.

Returns:
The set of all binary vectors ``v`` such that ``matrix @ v == 0`` over
GF(2), represented as tuples.

Notes:
This helper checks every binary vector of the appropriate length, so it is
a brute-force construction intended only for the small test cases in this
module.
"""
_, num_cols = matrix.shape

vectors: set[tuple[int, ...]] = set()
for entries in product((0, 1), repeat=num_cols):
vector = np.asarray(entries, dtype=np.uint8)
if np.all((matrix @ vector) % 2 == 0):
vectors.add(entries)

return vectors


def _dimension(space: set[tuple[int, ...]]) -> int:
return int(np.log2(len(space)))


def _assert_logical_basis(
logicals: npt.NDArray[np.uint8],
null_space_defining_checks: npt.NDArray[np.uint8],
quotient_space_defining_checks: npt.NDArray[np.uint8],
) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done )

"""Assert that returned logicals have the expected CSS logical structure.

This checks that ``logicals`` is a well-formed binary basis for the
non-redundant rows allowed by ``null_space_defining_checks``, with the
expected number of independent rows once the rows generated by
``quotient_space_defining_checks`` are treated as redundant.

Args:
logicals: Candidate logical rows returned by ``css_code_compute_logicals``.
null_space_defining_checks: Checks that define which rows are valid.
quotient_space_defining_checks: Checks whose row space is treated as
redundant when determining the expected logical rows.

Returns:
None.
"""
logicals_is_matrix = logicals.ndim == 2
num_logical_columns = logicals.shape[1]
num_logical_rows = logicals.shape[0]
num_null_space_columns = null_space_defining_checks.shape[1]
num_quotient_space_columns = quotient_space_defining_checks.shape[1]
logicals_are_binary = np.all(np.isin(logicals, [0, 1]))

assert logicals_is_matrix
assert num_logical_columns == num_null_space_columns == num_quotient_space_columns
assert logicals_are_binary

null_space = _binary_null_space(null_space_defining_checks)
quotient_space = _binary_row_space(quotient_space_defining_checks)
generated_space = _binary_row_space(
np.vstack([quotient_space_defining_checks, logicals])
)

# CSS logicals should extend the quotient space into a basis for the null space.
assert generated_space == null_space

expected_num_logicals = _dimension(null_space) - _dimension(quotient_space)
assert num_logical_rows == expected_num_logicals


@pytest.mark.parametrize(
("hx", "hz"),
[
(_empty_binary_matrix(3), _empty_binary_matrix(3)),
(_binary_matrix([[1, 1, 0], [0, 1, 1]]), _empty_binary_matrix(3)),
(_binary_matrix(np.eye(3, dtype=np.uint8).tolist()), _empty_binary_matrix(3)),
(
_binary_matrix([[1, 1, 0], [0, 1, 1], [1, 0, 1]]),
_empty_binary_matrix(3),
),
(
_binary_matrix([[1, 1, 1, 1]]),
_binary_matrix([[1, 1, 0, 0], [0, 0, 1, 1]]),
),
],
)
def test_css_code_compute_logicals_returns_valid_css_logical_bases(
hx: npt.NDArray[np.uint8], hz: npt.NDArray[np.uint8]
):
assert np.all((hx @ hz.T) % 2 == 0)

lx, lz = css_code_compute_logicals(hx.astype(float), hz.astype(float))

_assert_logical_basis(np.asarray(lx, dtype=np.uint8), hz, hx)
_assert_logical_basis(np.asarray(lz, dtype=np.uint8), hx, hz)


def test_css_code_compute_logicals_is_invariant_to_redundant_rows():
hx = _binary_matrix([[1, 1, 0], [0, 1, 1]])
redundant_hx = _binary_matrix([[1, 1, 0], [0, 1, 1], [1, 0, 1]])
hz = _empty_binary_matrix(3)

lx, lz = css_code_compute_logicals(hx.astype(float), hz.astype(float))
redundant_lx, redundant_lz = css_code_compute_logicals(
redundant_hx.astype(float), hz.astype(float)
)

assert _binary_row_space(np.asarray(lx, dtype=np.uint8)) == _binary_row_space(
np.asarray(redundant_lx, dtype=np.uint8)
)
assert _binary_row_space(np.asarray(lz, dtype=np.uint8)) == _binary_row_space(
np.asarray(redundant_lz, dtype=np.uint8)
)


def test_css_code_compute_logicals_preserves_logical_dimension_for_known_rank_checks():
# Adapted from the explicit GF(2) row-reduction fixture in galois/tests/fields/test_linalg.py.
hx = _binary_matrix(
[
[1, 0, 1, 0, 1, 0, 1, 0],
[0, 1, 1, 0, 0, 1, 1, 0],
[0, 0, 0, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1],
]
)
hz = _empty_binary_matrix(8)

rank_hx = _dimension(_binary_row_space(hx))
assert rank_hx == 4

lx, lz = css_code_compute_logicals(hx.astype(float), hz.astype(float))

assert lx.shape == (8 - rank_hx, 8)
assert lz.shape == (8 - rank_hx, 8)
_assert_logical_basis(np.asarray(lx, dtype=np.uint8), hz, hx)
_assert_logical_basis(np.asarray(lz, dtype=np.uint8), hx, hz)
7 changes: 0 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,6 @@ deltakit-compile = { workspace = true }
[tool.mypy]
files = "src/**/*.py"

# The ldpc.mod2 stubs files contain syntax errors, and so make mypy fail earlier than expected
# with a SyntaxError. Ignoring the whole module for mypy.
[[tool.mypy.overrides]]
module = ["ldpc.mod2"]
follow_imports = "skip"
follow_imports_for_stubs = true

# for ruff configuration, see `ruff.toml`

[tool.pytest.ini_options]
Expand Down
Loading