Skip to content

Reimplement flow matching using Gaussian elimination#45

Closed
HaoTy wants to merge 12 commits into
tqec:mainfrom
HaoTy:enhance/flow-matching
Closed

Reimplement flow matching using Gaussian elimination#45
HaoTy wants to merge 12 commits into
tqec:mainfrom
HaoTy:enhance/flow-matching

Conversation

@HaoTy
Copy link
Copy Markdown
Collaborator

@HaoTy HaoTy commented Feb 26, 2026

Flow matching is a major bottleneck of tqecd's performance and scalability. The matchings are solved by (SAT-solver-accelerated) brute force in main. This PR reimplements the functions in match_utils/cover.py to solve them efficiently using Gaussian elimination over GF(2), which partly addresses #2.

The solution's minimal-count requirement needed to be relaxed, but I don't think it fundamentally affects anything. All tqecd tests pass, and all tqec tests (including tests marked as slow) pass with do_not_use_database=True.

Benchmarking using a script adapted from tqec/tqec#356 (comment) shows that both the constant factor and scalability are drastically improved:

from time import perf_counter

from tqec import Basis, compile_block_graph
from tqec.gallery import memory

from tqecd import annotate_detectors_automatically


def format_timings(ks: list[int], timings: list[float]) -> str:
    return (
        "k        |"
        + " , ".join(f"{k:>6}" for k in ks)
        + "\ntime (s) |"
        + " , ".join(f"{t:6.3f}" for t in timings)
    )

block_graph = memory(Basis.Z)
compiled_graph = compile_block_graph(block_graph)

ks: list[int] = list(range(1, 10))
times: list[float] = []

for k in ks:
    circuit = compiled_graph.generate_stim_circuit(k=k, manhattan_radius=-1)
    start = perf_counter()
    annotate_detectors_automatically(circuit)
    end = perf_counter()
    times.append(end - start)

print("Time to annotate detectors:")
print(format_timings(ks, times))

This PR:

Time to annotate detectors:
k        |     1 ,      2 ,      3 ,      4 ,      5 ,      6 ,      7 ,      8 ,      9
time (s) | 0.009 ,  0.034 ,  0.089 ,  0.209 ,  0.421 ,  0.799 ,  1.371 ,  2.294 ,  3.481

main:

Time to annotate detectors:
k        |     1 ,      2 ,      3 ,      4 ,      5 ,      6 ,      7 ,      8 ,      9
time (s) | 0.014 ,  0.047 ,  0.339 ,  1.685 ,  3.606 ,  5.975 ,  9.122 , 13.128 , 18.237

Note that these improvements will not lead to a significant speedup in the detector annotation of tqec's ZX cubes, as they use the detector database and subtemplates to confine detector findings to small subcircuits. However, it is expected to benefit circuits that lack a nice subtemplate partitioning, such as cultivation and Y initialization/measurement. They should also benefit #37.

A bonus is that the SAT solver dependencies can be removed (included in this PR), making tqecd easily installable on platforms without a precompiled binary.

The int-based efficient Gaussian elimination infrastructure is borrowed from my implementation in https://github.com/tqec/tqec/blob/main/src/tqec/computation/_correlation.py.

Copy link
Copy Markdown
Contributor

@smburdick smburdick left a comment

Choose a reason for hiding this comment

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

Mostly documentation related.

Comment thread src/tqecd/cover.py


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.

Comment thread src/tqecd/cover.py
"""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 ..."

Comment thread src/tqecd/cover.py
superset of ``target``.

This function try to find a set of Pauli strings from ``sources`` that
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

Comment thread src/tqecd/cover.py
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.

Comment thread src/tqecd/cover.py

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.

Comment thread src/tqecd/cover.py
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

Comment thread src/tqecd/cover.py
"""Try to find a set of pauli strings from ``sources`` that generate exactly
``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.

Comment thread src/tqecd/cover_test.py
@@ -1,15 +1,11 @@
from __future__ import annotations
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 you think it's time to start moving the test files into their own directory

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.

Not very critical, but yeah, something we can do.

Comment thread pyproject.toml
[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.

Comment thread pyproject.toml
"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.

@HaoTy
Copy link
Copy Markdown
Collaborator Author

HaoTy commented Apr 17, 2026

This PR is superseded by #51. I will fix the typos, etc. there.

@HaoTy HaoTy closed this Apr 17, 2026
HaoTy added a commit that referenced this pull request May 3, 2026
This is a local version of #45 to avoid GitHub action permission issues
when merging from a fork.

---------

Co-authored-by: Copilot <copilot@github.com>
HaoTy added a commit to tqec/tqec that referenced this pull request May 4, 2026
Pending tqec/tqecd#45. Bump tqecd dependency
version; remove SAT solver dependencies; fix a typo in pyproject.toml.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants