Skip to content

Fix #168: Remove Pauli measurements#498

Open
thierry-martinez wants to merge 43 commits into
TeamGraphix:masterfrom
thierry-martinez:fix/168-remove-pauli-measurements
Open

Fix #168: Remove Pauli measurements#498
thierry-martinez wants to merge 43 commits into
TeamGraphix:masterfrom
thierry-martinez:fix/168-remove-pauli-measurements

Conversation

@thierry-martinez
Copy link
Copy Markdown
Collaborator

This commit implements the algorithm described in [BMBdF+21], Theorem 4.12 in Section 4.3: Removing Clifford vertices.

The new method Pattern.remove_pauli_measurements removes almost all Pauli measurements on non-input nodes, and all of them if the pattern has flow, fixing #168.

[BMBdF+21] Miriam Backens, Hector Miller-Bakewell, Giovanni de Felice, Leo Lobski, and John van de Wetering, There and back again: A circuit extraction tale, Quantum, 2021,
https://doi.org/10.22331/q-2021-03-25-421

This commit implements the algorithm described in [BMBdF+21], Theorem
4.12 in Section 4.3: Removing Clifford vertices.

The new method `Pattern.remove_pauli_measurements` removes almost all
Pauli measurements on non-input nodes, and all of them if the pattern
has flow, fixing TeamGraphix#168.

[BMBdF+21] Miriam Backens, Hector Miller-Bakewell, Giovanni de Felice,
Leo Lobski, and John van de Wetering, There and back again: A circuit
extraction tale, Quantum, 2021,
https://doi.org/10.22331/q-2021-03-25-421
@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.01%. Comparing base (222a292) to head (70d0ad7).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #498      +/-   ##
==========================================
- Coverage   89.65%   89.01%   -0.64%     
==========================================
  Files          48       49       +1     
  Lines        7015     7083      +68     
==========================================
+ Hits         6289     6305      +16     
- Misses        726      778      +52     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@matulni matulni left a comment

Choose a reason for hiding this comment

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

Thanks a lot for this nice piece of work @thierry-martinez ! Code is super clear and new tool very useful!

Now that remove_pauli_nodes will become a fundamental part of the workflow, I'm even more unhappy that the transformation Pattern -> OpenGraph or Pattern -> XZCorrections does not preserve information about the LC fragment. Maybe it may be worth thinking about this...

Comment thread graphix/optimization.py Outdated
Comment thread graphix/pattern.py Outdated
Comment thread graphix/pattern.py Outdated
Comment thread graphix/remove_pauli_measurements.py Outdated
Comment thread graphix/remove_pauli_measurements.py
Comment thread graphix/remove_pauli_measurements.py Outdated
Comment thread graphix/remove_pauli_measurements.py Outdated
Comment thread graphix/pattern.py
Comment thread graphix/pattern.py
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.

Should we remove Pattern.perform_pauli_measurements and eventually Pattern.remove_input_nodes?

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've replaced every call to Pattern.perform_pauli_measurements with a call to Pattern.remove_pauli_measurements. I addition, I removed the Pattern.perform_pauli_measurements method and the measure_pauli function.

However, I believe that the method Pattern.remove_input_nodes is still useful, because remove_pauli_measurements only acts on non-input Pauli nodes.

Before deleting perform_pauli_measurements, I ran a few benchmarks:

def compare_perfs(rng: Generator) -> None:
    nqubits = 10
    depth = 10
    patterns = [rand_circuit(nqubits, depth, rng, use_ccx=True, use_rzz=True).transpile().pattern for _ in range(10)]
    for pattern in patterns:
        pattern.remove_input_nodes()
    print(f"{timeit(lambda: [pattern.copy().remove_pauli_measurements() for pattern in patterns], number=10)=}")
    print(f"{timeit(lambda: [pattern.copy().perform_pauli_measurements() for pattern in patterns], number=10)=}")

On my laptop, the results were:

  • remove_pauli_measurements: 11.7s
  • perform_pauli_measurements: 27.9s

For fairness I copy each pattern before calling the method; using remove_pauli_measurements(copy=True) would be even faster.

I also benchmarked the example script qft_with_tn.py. The execution times were:

  • remove_pauli_measurements: 13.84s
  • perform_pauli_measurements: 51.73s

Comment on lines +232 to +237
def test_step_4_no_flow() -> None:
pattern = Pattern(input_nodes=(0,), output_nodes=(0,), cmds=[Command.N(1), Command.E((0, 1)), Command.M(1)])
standardized_pattern = StandardizedPattern.from_pattern(pattern)
cut = PauliPushingCut.from_standardized_pattern(standardized_pattern)
standardized_pattern2 = remove_pauli_measurements(cut)
assert len(standardized_pattern2.m_list) == 1
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'm not sure if I understand this test. Why are we supposed to have len(standardized_pattern2.m_list) == 1 ?
Why Command.M(1) is not eliminated ?
If I add Command.N(0) to the list, the same test won't pass because N(1) is eliminated:

pattern = Pattern(output_nodes=(0,), cmds=[Command.N(0), Command.N(1), Command.E((0, 1)), Command.M(1)])
standardized_pattern = StandardizedPattern.from_pattern(pattern)
cut = PauliPushingCut.from_standardized_pattern(standardized_pattern)
standardized_pattern2 = remove_pauli_measurements(cut)
assert len(standardized_pattern2.m_list) == 1 # Fails

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've updated the docstring of try_pivot_x_with_output_node to specify that the X-measured node must be connected to an output node that is not also an input node.

The test now includes this comment in test_step_4_no_flow:

    # This example tests the case of a pattern that contains a
    # non-input X-measured node 1 which is connected to an output node
    # 0, where the node 0 is also an input.  In this situation Lemma
    # 4.11 cannot be applied; this exercices the filtering implemented
    # in the `try_pivot_x_with_output_node` method.

Comment thread tests/test_pattern.py Outdated
Comment thread CHANGELOG.md
Comment thread tests/test_graphsim.py
Comment on lines +472 to +478
def remove_isolated_internal_nodes(self) -> None:
"""Remove isolated internal nodes."""
# Construct the list first since the graph should not be
# modified while enumerating isolated nodes.
for node in list(nx.isolates(self.graph)):
if node not in self.input_node_set and node not in self.output_node_set:
self._remove_node(node)
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.

This method unconditionally removes isolated nodes, even if they are not Pauli-measured!

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 reproduces the current behavior of perform_pauli_measurements.

Unfortunately, the existing test suite does not contain a test that checks this behavior, but the following test would pass:

def test_isolated_nodes_non_pauli() -> None:
    pattern = Pattern(cmds=[Command.N(0), Command.N(1), Command.M(0), Command.M(1, ANGLE_PI / 4)])
    pattern.perform_pauli_measurements()
    assert list(pattern) == []

Note that I had to introduce an additional Pauli node 0 since perform_pauli_measurements exits early when there are no Pauli measurements.

I am not sure what the best course of action is. Eliminating isolated nodes seems like a useful optimization, regardless of whether nodes are Pauli or non-Pauli. Moreover, removing Pauli nodes can introduce isolated nodes, and since the procedure already has access to the graph and the logic needed to rebuild the pattern from that graph, it appears to be the appropriate place to perform this cleanup.

That said, I agree that it is somewhat counter-intuitive for the Pauli measurement removal routine to also delete non-Pauli nodes.

Comment thread graphix/remove_pauli_measurements.py
thierry-martinez added a commit to thierry-martinez/graphix-stim-backend that referenced this pull request May 12, 2026
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