Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@
}
},
"remoteUser": "vscode",
"postCreateCommand": "python -m pip install -r ./requirements.txt && python -m pip install -e ./[all] && pre-commit install && pre-commit run -a"
"postCreateCommand": "python -m pip install -e ./[all] && pre-commit install && pre-commit run -a"
}
12 changes: 5 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: "pip"
- name: Install dependencies
run: |
Expand All @@ -29,25 +29,23 @@ jobs:
needs: pre-commit-ci
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- name: Install dependencies
- name: Build
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install -e .
- name: Analyze code with pylint
run: |
python -m pip install pylint
pylint $(git ls-files '*.py')
continue-on-error: true
- name: Build
run: python -m pip install -e .
- name: Test and coverage
run: |
python -m pip install pytest pytest-cov
Expand All @@ -69,7 +67,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: "pip"
- name: Install dependencies and tqec package
run: |
Expand Down
12 changes: 5 additions & 7 deletions .github/workflows/exotic-oses.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,19 @@ jobs:
uses: actions/setup-python@v5
with:
# Use smallest supported Python version as the common denominator.
# In theory, if everything succeed in 3.9, everything should succeed in
# 3.10, 3.11 and follow-up version.
python-version: "3.9"
# In theory, if everything succeed in 3.10, everything should succeed in
# 3.11 and follow-up versions.
python-version: "3.10"
cache: "pip"
- name: Install dependencies
- name: Build
run: |
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install '.[all]'
- name: Analyze code with pylint
run: |
python -m pip install pylint
pylint $(git ls-files '*.py')
continue-on-error: true
- name: Build
run: python -m pip install '.[all]'
- name: Test and coverage
run: |
python -m pip install pytest pytest-cov
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ jobs:
sudo apt-get update
sudo apt-get install -y --no-install-recommends pandoc
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
python -m pip install '.[all]'
- name: Generate the documentation
run: |
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ Currently, `tqecd` needs to be installed from source using
python -m pip install git+https://github.com/tqec/tqecd.git
```

The `tqecd` package has some dependencies that might be harder to install than a simple `pip install`. If you have any issues with the simple installation method above, please look at the [full installation page](https://tqec.github.io/tqecd/user_guide/installation.html).

## Basic usage

```py
Expand Down
15 changes: 0 additions & 15 deletions docs/user_guide/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,6 @@ Python version
The ``tqecd`` package only supports Python 3.10 and onward. If you have Python 3.9 or below,
please update your Python installation.

Additional toolchains
~~~~~~~~~~~~~~~~~~~~~

Some of the dependencies of ``tqecd`` are implemented using compiled languages. This is for
example the case of the `pycryptosat <https://pypi.org/project/pycryptosat/>`_ dependency.
Pre-compiled Python packages that should be compatible with any GNU/Linux are provided
by the author, but no pre-compiled package exist for Windows or MacOS.

This means that, if you try to install the ``tqecd`` package on Windows or MacOS, a working
C++ toolchain should also be installed on your system.

Here is a list of potential issues you might encounter and how to solve them:

- `Failed building wheel for pycryptosat <https://github.com/tqec/tqec/issues/311>`_

Installation procedure
----------------------

Expand Down
19 changes: 11 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"

[project]
name = "tqecd"
version = "0.1.3"
version = "0.1.4"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we want to do a release?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes. Notice the reduced dependencies and the further implications.

authors = [
{ name = "TQEC community", email = "tqec-design-automation@googlegroups.com" },
]
description = "Automatically find detectors in a topologically quantum error corrected computation"
readme = "README.md"
license = { file = "LICENSE" }
keywords = ["topological quantum error correction", "qec", "detectors"]
requires-python = ">= 3.9"
requires-python = ">= 3.10"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
Expand All @@ -28,12 +28,19 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities",
"Typing :: Typed",
]
dynamic = ["dependencies"]
dependencies = [
"numpy>=1.22,<2.3", # Upper bound: https://github.com/tqec/tqec/pull/659
"numpy>=1.24; python_version >= '3.11'",
"numpy>=1.26; python_version >= '3.12'",
"numpy>=2.1; python_version >= '3.13'",
"stim>=1.15.0",
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is dependabot needed for this repo?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think there are some bots already? But admittedly, the CICD aspect is not as polished as tqec. Let's start by switching to uv, ty, and ruff and make the pyproject and GitHub action configurations more rigorous.


[project.urls]
Documentation = "https://tqec.github.io/tqecd/"
Expand Down Expand Up @@ -64,10 +71,6 @@ exclude = ["*test.py"]
[tool.setuptools.package-data]
tqecd = ["py.typed"]

[tool.setuptools.dynamic]
dependencies = { file = "requirements.txt" }


[tool.mypy]
# See https://numpy.org/devdocs/reference/typing.html
plugins = "numpy.typing.mypy_plugin"
Expand Down Expand Up @@ -106,7 +109,7 @@ warn_return_any = true

# See https://mypy.readthedocs.io/en/stable/config_file.html#using-a-pyproject-toml
[[tool.mypy.overrides]]
module = ["stim", "pysat", "pysat.solvers"]
module = ["stim"]
ignore_missing_imports = true

[tool.coverage.run]
Expand Down
4 changes: 0 additions & 4 deletions requirements.txt

This file was deleted.

3 changes: 0 additions & 3 deletions setup.py

This file was deleted.

150 changes: 150 additions & 0 deletions src/tqecd/cover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""Provides utility functions to find "covers" of Pauli strings."""

from __future__ import annotations

from collections.abc import Callable, Iterable
from typing import Any

from tqecd.pauli import PauliString


def _find_cover(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe document somewhere what exactly is meant by "cover"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Basically the first line of the docstring below.

target: PauliString, sources: list[PauliString], on_qubits: frozenset[int]
) -> list[int] | None:
"""Try to find a set of boundary stabilizers from ``sources`` that generate
target on qubits ``on_qubits``.

If multiple valid covers exist, the covers involving the lowest number of
:class:`~tqecd.pauli.PauliString` instances from ``sources`` are listed, and
a random cover is picked from that list.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not just return all of the covers?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Because there are exponentially many of them, and they don't really make a difference. But this doc describes the previous behavior and is inaccurate now. Will fix.


Args:
target: the stabilizers to cover with stabilizers from ``sources``.
sources: stabilizers that can be used to cover `target`.
on_qubits: qubits to consider when trying to cover ``target`` with
``sources``.

Returns:
Either a list of indices over ``sources`` that, when combined, cover
exactly the provided ``target`` on all the qubits provided in
``on_qubits``, or ``None`` if such a list could not be found.
"""
return _solve_linear_system(
_construct_basis({}, sources, lambda s: s.to_int(on_qubits)),
target.to_int(on_qubits),
)


def find_exact_cover(
target: PauliString, sources: list[PauliString]
) -> list[int] | None:
"""Try to find a set of pauli strings from ``sources`` that generate exactly
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

*Pauli strings

``target``.

The Pauli strings returned (via indices over the provided ``sources``), once
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

By the by, I noticed #47 , are you feeling the slowdown in this computation too?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This computation doesn't rely on the PauliString class too much, because Pauli strings are converted to ints. There could be a speedup for the conversion.

multiplied together, should be exactly equal to ``target``. In particular, the
following post-condition should hold:

.. code-block:: python

target = None # to replace
sources = [None] # to replace
cover_indices = find_exact_cover(target, sources)
resulting_pauli_string = PauliString({})
for i in cover_indices:
resulting_pauli_string = resulting_pauli_string * sources[i]
assert resulting_pauli_string == target, "Should hold"

Args:
target: the stabilizers to cover with stabilizers from ``sources``.
sources: stabilizers that can be used to cover ``target``.

Returns:
Either a list of indices over ``sources`` that, when combined, cover
exactly the provided ``target``, or ``None`` if such a list could not be
found.
"""
# If target is the identity, pick no Pauli string.
# Note: we might want to disallow an empty return in the future.
if target.non_trivial_pauli_count == 0:
return []

# Else, if there are no sources, we cannot find a solution.
if not sources:
return None

# We want an exact (i.e., equality) cover on all qubits, to be sure that
# the post-condition in the docstring holds. For that, it is sufficient to
# only consider all the qubits where either `target` or at least one of the
# items of `sources` acts non-trivially (i.e., something else than the identity).
involved_qubits = frozenset(target.qubits)
for source in sources:
involved_qubits |= frozenset(source.qubits)

return _find_cover(target, sources, involved_qubits)


def find_commuting_cover_on_target_qubits(
target: PauliString, sources: list[PauliString]
) -> list[int] | None:
"""Try to find a set of boundary stabilizers from ``sources`` that generate a
superset of ``target``.

This function try to find a set of Pauli strings from ``sources`` that
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

typo: "This function tries to find ..."

includes ``target`` (i.e., on every qubit where ``target`` is non-trivial,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is a trivial target?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Non-identity

the product of each of the returned Pauli strings should commute with
``target``).

The differences with :func:`find_cover` are:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would find_noncommuting_cover be a valid name for find_cover?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The method names are somewhat legacy from the previous implementation. Now after simplification, it's clear that they only differ by how they treat the involved qubits and edge cases. I will think about whether the naming could be improved.


1. this function does not restrict the output of the product of each of
the returned Pauli string on qubits where ``target`` acts trivially (i.e.
"I"). So in practice, on qubits where ``target[qubit] == "I"``, the value
of the returned Pauli string can be anything.
2. this function does not restrict the output of the product of each of
the returned Pauli string to exactly match ``target`` on qubits where
it acts non-trivially, but rather requires the output to commute with
``target`` on those qubits.

Args:
target: the stabilizers to cover with stabilizers from ``sources``.
sources: stabilizers that can be used to cover ``target``.

Returns:
Either a list of a stabilizers that, when combined, commute with
the provided ``target``, or ``None`` if such a list could not be found.
"""
if not sources:
return None
return _find_cover(target, sources, frozenset(target.qubits))


def _solve_linear_system(
basis: dict[int, tuple[int, int]], x: int, update_basis: bool = True
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Might want to define a Basis type or class somewhere, just to make it clear what the valid keys/values are, kind of like what's in tqec.

) -> list[int] | None:
"""Gaussian elimination over GF(2)."""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd add a little more detail to this docstring or add a few more comments ot the code since it's kind of abstract. Like, what the valid inputs are (since it can return None).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It should also document how the method mutates the basis.

mask = 1 << len(basis)
while x:
highest_bit = x.bit_length() - 1
if highest_bit not in basis:
if update_basis:
basis[highest_bit] = (x, mask)
return None
pivot, pivot_mask = basis[highest_bit]
x ^= pivot
mask ^= pivot_mask
return _int_to_bit_indices(mask)[:-1]


def _int_to_bit_indices(x: int) -> list[int]:
"""Convert an integer to a list of indices where the bits are set."""
return [i for i in range(x.bit_length()) if (x >> i) & 1]


def _construct_basis(
basis: dict[int, tuple[int, int]], items: Iterable[Any], func: Callable[[Any], int]
) -> dict[int, tuple[int, int]]:
"""Construct a linear basis from the given items using the provided function."""
for item in items:
_solve_linear_system(basis, func(item))
return basis
Loading
Loading