Skip to content

Add Clifford decoration on output nodes of OpenGraph#510

Open
matulni wants to merge 15 commits into
TeamGraphix:masterfrom
matulni:og-clifford
Open

Add Clifford decoration on output nodes of OpenGraph#510
matulni wants to merge 15 commits into
TeamGraphix:masterfrom
matulni:og-clifford

Conversation

@matulni
Copy link
Copy Markdown
Contributor

@matulni matulni commented May 18, 2026

This PR fixes two known (but unreported) issues concerning patterns in the LC fragement and open graphs.

Issue 1

Pattern.extract_opengraph ignores Clifford commands on measured nodes. Therefore, this method returns different open graphs depending on whether the pattern has been standardized first or not:

from graphix import Command, Clifford, Measurement, Pattern

p = Pattern(cmds=[Command.N(0), Command.C(0, Clifford.H), Command.M(0, Measurement.XY(0.3))])
og = p.extract_opengraph()
p.standardize()
og_std = p.extract_opengraph()

assert og.isclose(og_std) # fails

To ensure canonicality, extracting an open graph from a pattern should always proceed by standardizing the pattern first.

Issue 2

Pattern.extract_opengraph ignores Clifford commands on output nodes. Therefore, it does not preserve the semantics of the pattern:

from graphix import Command, Clifford, Pattern

p = Pattern(input_nodes=[0],
            cmds=[
                N(1),
                E((0, 1)),
                C(0, Clifford.H),
                M(0, -Measurement.Z),
                X(1, {0}),
                C(1, Clifford.H),
                C(1, Clifford.X),
            ])
p_test = p.extract_opengraph().to_pattern()

sv = p.simulate_pattern()
sv_test = p_test.simulate_pattern()

assert sv.isclose(sv_test) # fails

To guarantee the round trip Pattern -> OpenGraph -> Flow -> XZCorrections -> Pattern for patterns in the LC fragment:

  1. Clifford commands on measured nodes should be absorbed into measurements via standardization (issue 1),

  2. Open graphs should conserve a mapping between output nodes and Clifford operators.

Summary of changes

To fix issues above, we introduce a new attribute to the OpenGraph.output_cliffords mapping output nodes to Clifford operations. This corresponds to c_dict in StandardizedPattern. Further:

  • Pattern.extract_opengraph standardizes the pattern first.
  • XZCorrections.to_pattern applies Cliffords in OpenGraph.output_cliffords at the end of the pattern.
  • OpenGraph.isclose and OpenGraph.is_equal_structurally check equality of OpenGraph.output_cliffords.
  • OpenGraph.compose merges Clifford decorations with measurements or other Clifford decorations on outputs if required.
  • .draw methods allow to show Clifford commands in the outputs.
  • PauliFlow.extract_circuit raises NotImplementedError if the open graph has Clifford decorations. This will be left for a future PR to simplify the review.

Additionally, a minor bug in Pattern.compose and OpenGraph.compose which appeared when trying to compose single-node objects with an empty mapping is fixed, and the corresponding test added.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

❌ Patch coverage is 96.29630% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.85%. Comparing base (6de358c) to head (84f08e0).

Files with missing lines Patch % Lines
graphix/opengraph.py 96.00% 1 Missing ⚠️
graphix/visualization.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #510      +/-   ##
==========================================
+ Coverage   88.84%   88.85%   +0.01%     
==========================================
  Files          49       49              
  Lines        7127     7135       +8     
==========================================
+ Hits         6332     6340       +8     
  Misses        795      795              

☔ 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
Collaborator

@thierry-martinez thierry-martinez left a comment

Choose a reason for hiding this comment

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

Looks nice! I've some minor comments.

Comment thread graphix/opengraph.py
Comment thread graphix/opengraph.py Outdated
Comment on lines +704 to +720
# To comply with mypy, we could define a runtime-checkable Protocol:
#
# from typing import Protocol, Self, runtime_checkable
# @runtime_checkable
# class HasClifford(Protocol):
# def clifford(self, clifford_gate: Clifford) -> Self: ...
#
# if not isinstance(um, HasClifford):
# raise OpenGraphError("...")
# measurements[u] = um.clifford(vc)
#
# This informs mypy that `um` has the `clifford` attribute
# without narrowing the type of `um` so we can still assign it to
# measurements: dict[int, _AM_co]
# However, `isinstance` with protocols is disadvised since it can decrease
# performance significantly.
# https://typing.python.org/en/latest/reference/protocols.html
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.

I propose to implement the clifford method on all AbstractMeasurement instead: thierry-martinez@67b0ab1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion. Much cleaner and general now

Comment thread graphix/pattern.py Outdated
"""
lc = self.extract_clifford() if show_local_clifford else None
options.setdefault("local_clifford", lc)
local_clifford_map = self.extract_clifford() if options.get("local_clifford") else 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.

I believe it is better to rely on the Clifford map stored in the extracted open graph.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 40841d3

@matulni matulni requested a review from thierry-martinez May 22, 2026 12:47
Copy link
Copy Markdown

@pranav97nair pranav97nair left a comment

Choose a reason for hiding this comment

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

Nice work, Mateo! I just made a very minor comment.

Comment thread graphix/opengraph.py
Comment on lines +700 to +708
if vm is not None and um is not None and not vm.isclose(um):
raise OpenGraphError(f"Cannot merge nodes with different measurements: {v, vm} -> {u, um}.")

shift = max(*self.graph.nodes, *mapping.values()) + 1
# Apply other's output Cliffords onto self's measurement
if um is not None and (vc := other.output_cliffords.get(v)) is not None:
measurements[u] = um.clifford(vc)

# Apply self's output Cliffords onto other's measurement
if vm is not None and (uc := self.output_cliffords.get(u)) is not None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is there a particular reason you specify is not None instead of just saying if vm and um...? If not I think it would be better to do the latter for improved readability.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed we could do that, but I'm not sure if it improves readability.

When we test the truthiness of an object x, the Python interpreter checks for the x.__bool__ and the x.__len__ methods. When they are not implemented (as it is the case for AbstractMeasurement and children), it always assigns a truthy value so your suggestion would work.

However, someone reading the code would need to know or assume that AbstractMeasurement does not implement __bool__, and may wonder what is the truth value of 0-angle measurements. (In Python, the zero of any numeric type is False).

Checking against None is indeed more verbose, but less ambiguous imo.

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.

3 participants