Skip to content

Fix #181: Structural separation of Pauli measurements#423

Merged
thierry-martinez merged 13 commits intoTeamGraphix:masterfrom
thierry-martinez:fix/181-pauli_plane_measurements
Feb 17, 2026
Merged

Fix #181: Structural separation of Pauli measurements#423
thierry-martinez merged 13 commits intoTeamGraphix:masterfrom
thierry-martinez:fix/181-pauli_plane_measurements

Conversation

@thierry-martinez
Copy link
Copy Markdown
Collaborator

@thierry-martinez thierry-martinez commented Feb 4, 2026

This commit introduces a structural separation between Pauli measurements and arbitrary measurements. Technically, the class Measurement is now abstract and has two concrete subclasses: PauliMeasurement and BlochMeasurement. In patterns, M commands are now parameterized by an instance Measurement (instead of carrying a plane and an angle).

With these changes, an M command on an arbitrary measurement is written in the following form:

M(node, BlochMeasurement(angle, plane), s_domain, t_domain)

and an M command on a Pauli measurement is written as:

M(node, PauliMeasurement(axis, sign), s_domain, t_domain)

There are convenient notations for common cases:

  • for an arbitrary measurement on a statically known plane, the user can use Measurement.XY(angle), Measurement.YZ(angle), or Measurement.XZ(angle);
  • for a positive Pauli measurement on a statically known axis, the user can use Measurement.X, Measurement.Y, Measurement.Z;
  • for a negative Pauli measurement on a statically known axis, the user can use -Measurement.X, -Measurement.Y, -Measurement.Z.

By default, M(node) is M(node, Measurement.X).

There is no implicit conversion between Pauli measurements represented as PauliMeasurement and measurements represented as BlochMeasurement. Measurement class provides methods to perform these conversions explicitly:

  • to_bloch() returns a BlochMeasurement equivalent to the given measurement; note that there are multiple possible Bloch descriptions for the same Pauli measurement. to_bloch() uses Plane.XY for X and Y and Plane.YZ for Z.
  • try_to_pauli() returns a PauliMeasurement equivalent to the given measurement if it exists (up to a precision error specified by optional parameters rel_tol and abs_tol), or None otherwise.
  • to_pauli_or_bloch() is similar to try_to_pauli(), but returns the original measurement instead of None in the non-Pauli case.

There are conversion functions at the level of Pattern, StandardizedPattern, and OpenGraph to convert all measurements:

  • to_bloch() converts all measurements to BlochMeasurement;
  • infer_pauli_measurements() converts all measurements that are Pauli (up to a precision error) into PauliMeasurement.

In particular, Pauli presimulation, flow extraction algorithms and visualization routines do not perform any implicit conversion:

  • Pauli presimulation only applies on M commands carrying a PauliMeasurement;

  • causal flows and gflows may only exist if all measurements are BlochMeasurement;

  • Pauli flows considers a measurement to be a Pauli measurement only if it carries a PauliMeasurement;

  • visualization routines now take an OpenGraph directly instead of taking planes and angles separately, and displayed Pauli nodes and labels reflect whether the measurement is a BlochMeasurement or a PauliMeasurement. In particular, since there is no Pauli-flow extraction from pattern, visualization of flows extracted from patterns only work if all measurements are BlochMeasurement (and no Pauli nodes will be displayed).

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 4, 2026

Codecov Report

❌ Patch coverage is 97.53915% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.73%. Comparing base (be598e2) to head (b03d9ef).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
graphix/visualization.py 90.41% 7 Missing ⚠️
graphix/pattern.py 95.23% 2 Missing ⚠️
graphix/pretty_print.py 94.28% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #423      +/-   ##
==========================================
+ Coverage   86.72%   88.73%   +2.00%     
==========================================
  Files          44       44              
  Lines        6163     6303     +140     
==========================================
+ Hits         5345     5593     +248     
+ Misses        818      710     -108     

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

This commit introduces a structural separation between Pauli
measurements and arbitrary measurements. Technically, the class
`Measurement` is now abstract and has two concrete subclasses:
`PauliMeasurement` and `BlochMeasurement`. In patterns,  `M` commands
are now parameterized by an instance `Measurement` (instead of
carrying a plane and an angle).

With these changes, an M command on an arbitrary measurement is
written in the following form:
```python
M(node, BlochMeasurement(angle, plane), s_domain, t_domain)
```
and an M command on a Pauli measurement is written as:
```python
M(node, PauliMeasurement(axis, sign), s_domain, t_domain)
```

There are convenient notations for common cases:
- for an arbitrary measurement on a statically known plane, the user can use
  `Measurement.XY(angle)`, `Measurement.YZ(angle)`, or
  `Measurement.XZ(angle)`;
- for a positive Pauli measurement on a statically known axis, the user can use
  `Measurement.X`, Measurement.Y`, `Measurement.Z`;
- for a negative Pauli measurement on a statically known axis, the user can use
  `-Measurement.X`, -Measurement.Y`, `-Measurement.Z`.

By default, `M(node)` is `M(node, Measurement.X)`.

There is no implicit conversion between Pauli measurements represented
as `PauliMeasurement` and measurements represented as
`BlochMeasurement`.  `Measurement` class provides methods to perform
these conversions explicitly:

- `to_bloch()` returns a `BlochMeasurement` equivalent to the given
  measurement; note that there are multiple possible Bloch descriptions
  for the same Pauli measurement. `to_bloch()` uses `Plane.XY` for `X`
  and `Y` and `Plane.YZ` for `Z`.
- `try_to_pauli()` returns a `PauliMeasurement` equivalent to the
  given measurement if it exists (up to a precision error specified
  by optional parameters `rel_tol` and `abs_tol`), or `None`
  otherwise.
- `to_pauli_or_bloch()` is similar to `try_to_pauli()`, but returns the
  original measurement instead of `None` in the non-Pauli case.

There are conversion functions at the level of `Pattern`,
`StandardizedPattern`, and `OpenGraph` to convert all measurements:

- `to_bloch()` converts all measurements to `BlochMeasurement`;
- `infer_pauli_measurements()` converts all measurements that are
  Pauli (up to a precision error) into `PauliMeasurement`.

In particular, Pauli presimulation, flow extraction algorithms and
visualization routines do not perform any implicit conversion:

- Pauli presimulation only applies on `M` commands carrying a
  `PauliMeasurement`;

- causal flows and gflows may only exist if all measurements are
  `BlochMeasurement`;

- Pauli flows considers a measurement to be a Pauli measurement only
  if it carries a `PauliMeasurement`;

- visualization routines now take an `OpenGraph` directly instead of
  taking planes and angles separately, and displayed Pauli nodes and
  labels reflect whether the measurement is a `BlochMeasurement` or a
  `PauliMeasurement`. In particular, since there is no Pauli-flow
  extraction from pattern, visualization of flows extracted from
  patterns only work if all measurements are `BlochMeasurement` (and no
  Pauli nodes will be displayed).
@thierry-martinez thierry-martinez force-pushed the fix/181-pauli_plane_measurements branch from 8945ed4 to b2a106e Compare February 9, 2026 11:50
Copy link
Copy Markdown
Contributor

@shinich1 shinich1 left a comment

Choose a reason for hiding this comment

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

Thank you, the overall idea looks great. I feel that a few more tests could be added to test_measurements to ensure that the new feature (new class, conversion to bloch/pauli, etc) will work well in the future.

@thierry-martinez
Copy link
Copy Markdown
Collaborator Author

I added more documentation in measurements.py and other impacted modules, and the examples are tested with pytest --doctest-modules. In particular, test coverage for measurements.py is now 100%.

Copy link
Copy Markdown
Contributor

@emlynsg emlynsg left a comment

Choose a reason for hiding this comment

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

Left a few comments. We can discuss at the next PR meeting.

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 great PR @thierry-martinez . Since it already has 2 approvals, I didn't spend much time to avoid blocking it.

I made two minor comments, and I have a question: since now OpenGraph.find_pauli_flow doesn't do any implicit interpretation of measurements with Pauli angle as PauliMeasurements, I think the method PauliFlow.node_measurement_label is redundant at best (I'd argue it's misleading). Doing PauliFlow.og.measurements[node] should be the unambiguous way to retrieve the measurement of a given node from a flow object. If you agree, I'd remove that method from the flow class.

Edit: in fact, since now BlochMeasurement implements the method to_plane_or_axis as return self.plane (implementation inherited from AbstractPlanarMeasurement), do we need to make the distinction between AlgebraicOpenGraph and PlanarAlgebraicOpenGraph at all (in _find_gpflow.py)? I would say that we don't have to, but maybe I'm missing something. We can discuss it in person.

@matulni
Copy link
Copy Markdown
Contributor

matulni commented Feb 16, 2026

I'll try to nuance what I was saying about the distinction between AlgebraicOpenGraph and PlanarAlgebraicOpenGraph.

Consider the following example:

import networkx as nx
from graphix.opengraph import OpenGraph
from graphix.measurements import Measurement
graph = nx.Graph([(0, 1), (1, 2)])


measurements = {0: Measurement.XY(0.5), 1: Measurement.XY(0.5)}
og = OpenGraph(graph, [0], [2], measurements)
print(og.extract_gflow()) # No error
print(og.extract_pauli_flow()) # No error

The only difference in the gflow and Pauli flow calculation is that in the former we initialize a PlanarAlgebraicOpenGraph object, whereas in the latter we initialize an AlgebraicOpenGraph. These two only differ in that PlanarAlgebraicOpenGraph uses .to_plane to retrieve a measurement label and AlgebraicOpenGraph uses .to_plane_or_axis. My understanding is that after this PR, a BlochMeasurement is always treated as a planar measurement, regardless of the angle. And indeed:

print(og.measurements[0].to_plane_or_axis()) # Plane.XY
Plane.XY
print(og.measurements[0].to_plane()) # Plane.XY
Plane.XY

return the same. This is why I suggest discussing if it's worth removing the PlanarAlgebraicOpenGraph class altogether.

In the current form, the following code yields an error:

import networkx as nx
from graphix.opengraph import OpenGraph
from graphix.measurements import Measurement
graph = nx.Graph([(0, 1), (1, 2)])

measurements = {0: Measurement.XY(0.5), 1: Measurement.XY(0.5)}
og = OpenGraph(graph, [0], [2], measurements).infer_pauli_measurements()
print(og.extract_gflow()) # Error
print(og.extract_pauli_flow())

because PauliMeasurement doesn't have a to_plane method which is called from the PlanarAlgebraicOpenGraph instance created when calling og.extract_gflow().

If we removed the class PlanarAlgebraicOpenGraph (and simply instantiated an AlgebraicOpenGraph instead), then the code above would (incorrectly) run without errors, but we would be warned by the type checker that something is not quite right: we are trying to extract a GFlow from an OpenGraph[PauliMeasurement] object. This seems fair enough.

The issue occurs when the type checker cannot infer the type variable of the open graph, e.g.,

import networkx as nx
from graphix.opengraph import OpenGraph
from graphix.measurements import Measurement
graph = nx.Graph([(0, 1), (1, 2)])

measurements = {0: Measurement.X, 1: Measurement.XY(0.5)}
og = OpenGraph(graph, [0], [2], measurements).infer_pauli_measurements()
print(og.extract_gflow())
print(og.extract_pauli_flow())

Here the type-checker doesn't warn us about the misuse of .extract_gflow() and upon running we incorrectly get a flow.

TLDR

We can leave the PR as is, since everything works as it should, I was just worried that the previous architecture was too complex in the new framework where we don't implicitly convert planar measurement into Pauli measurements.
(If you took the time to read, let me know if you agree though! :))

@thierry-martinez thierry-martinez merged commit c1f689d into TeamGraphix:master Feb 17, 2026
24 checks passed
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.

4 participants