Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #476 Introduced new methods `OpenGraph.extract_circuit`, `CliffordMap.to_tableau` and new function `graphix.circ_ext.compilation.cm_berg_pass`. Circuit extraction can be done natively in Graphix.

- #505
- Added new methods `XZCorrections.to_causal_flow` and `XZCorrections.to_gflow` which subsume `StandardizedPattern.extract_causal_flow` and `StandardizedPattern.extract_gflow`.
- Added new methods `XZCorrections.to_bloch` and `XZCorrections.downcast_bloch`.

### Fixed

### Changed
Expand Down
78 changes: 78 additions & 0 deletions graphix/flow/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@
# Unpack introduced in Python 3.12
from typing_extensions import Unpack

from graphix.measurements import BlochMeasurement
from graphix.opengraph import OpenGraph
from graphix.parameter import ExpressionOrSupportsFloat, Parameter
from graphix.pattern import Pattern
from graphix.visualization import DrawKwargs


TotalOrder = Sequence[int]

_T_PauliFlowMeasurement = TypeVar("_T_PauliFlowMeasurement", bound="PauliFlow[Measurement]")
Expand Down Expand Up @@ -195,6 +197,82 @@ def to_pattern(
pattern.reorder_output_nodes(self.og.output_nodes)
return pattern

def to_causal_flow(self: XZCorrections[_PM_co]) -> CausalFlow[_PM_co]:
r"""Extract a causal flow from XZ-corrections.

This method does not invoke the flow-extraction routine on the underlying open graph.
Instead, it assigns the ``x_corrections`` mapping to the flow's correction function
and verifies that it is compatible with the intrinsic partial order of the XZ-corrections.
If the resulting correction function is incompatible with this partial order,
or the open graph contains measurements in XZ or YZ planes, a ``FlowError`` is raised.

Returns
-------
CausalFlow[_PM_co]

Notes
-----
See Theorem 1 in Ref. [1].

References
----------
[1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212).
"""
cf = CausalFlow(self.og, self.x_corrections, self.partial_order_layers)
cf.check_well_formed() # Raises a `FlowError` if the partial order and the correction function are not compatible, if a measured node is corrected by more than one node, or if nodes are not measured on the XY plane.
return cf

def to_gflow(self: XZCorrections[_PM_co]) -> GFlow[_PM_co]:
r"""Extract a gflow from XZ-corrections.

This method does not invoke the flow-extraction routine on the underlying open graph.
Instead, it assigns the ``x_corrections`` mapping to the flow's correction function
and verifies that it is compatible with the intrinsic partial order of the XZ-corrections.
Nodes measured in planes XZ or YZ are assigned to their correcting set. If the resulting
correction function is incompatible with this partial order ``FlowError`` is raised.

Returns
-------
GFlow[_PM_co]

Notes
-----
See Theorem 2 in Ref. [1].

References
----------
[1] Browne et al., 2007 New J. Phys. 9 250 (arXiv:quant-ph/0702212).
"""
correction_function: dict[int, set[int]] = {}

for i, meas in self.og.measurements.items():
corrections = set(self.x_corrections.get(i, set()))

if meas.to_plane() in {Plane.XZ, Plane.YZ}:
corrections.add(i)

correction_function[i] = corrections

gf = GFlow(self.og, correction_function, self.partial_order_layers)
gf.check_well_formed() # Raises a `FlowError` if the partial order and the correction function are not compatible.
return gf

def to_bloch(self: XZCorrections[Measurement]) -> XZCorrections[BlochMeasurement]:
"""Return the XZ-corrections where all measurements in the open graph are converted to Bloch.

See :meth:`OpenGraph.to_bloch` for additional information.
"""
return XZCorrections(self.og.to_bloch(), self.x_corrections, self.z_corrections, self.partial_order_layers)

def downcast_bloch(self: XZCorrections[Measurement]) -> XZCorrections[BlochMeasurement]:
"""Return the open graph if all measurements are described as Bloch measurements; raise `TypeError` otherwise.

See :meth:`OpenGraph.downcast_bloch` for additional information.
"""
return XZCorrections(
self.og.downcast_bloch(), self.x_corrections, self.z_corrections, self.partial_order_layers
)

def generate_total_measurement_order(self) -> TotalOrder:
"""Generate a sequence of all the non-output nodes in the open graph in an arbitrary order compatible with the intrinsic partial order of the XZ-corrections.

Expand Down
107 changes: 2 additions & 105 deletions graphix/optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

import dataclasses
from collections import defaultdict
from copy import copy
from dataclasses import dataclass
from types import MappingProxyType
Expand All @@ -19,12 +18,8 @@
from graphix.clifford import Clifford, Domains
from graphix.command import CommandKind, Node
from graphix.flow._partial_order import compute_topological_generations
from graphix.flow.core import CausalFlow, GFlow, XZCorrections
from graphix.flow.exceptions import (
FlowGenericError,
FlowGenericErrorReason,
)
from graphix.fundamentals import Axis, Plane, Sign
from graphix.flow.core import XZCorrections
from graphix.fundamentals import Axis, Sign
from graphix.measurements import BlochMeasurement, Measurement, Outcome, PauliMeasurement
from graphix.opengraph import OpenGraph
from graphix.space_minimization import (
Expand Down Expand Up @@ -507,104 +502,6 @@ def process_domain(node: Node, domain: AbstractSet[Node]) -> None:
return oset, *generations[::-1]
return generations[::-1]

def extract_causal_flow(self) -> CausalFlow[BlochMeasurement]:
r"""Extract the causal flow structure from the current measurement pattern.

This method does not call the flow-extraction routine on the underlying open graph, but constructs the flow from the pattern corrections instead.

Returns
-------
flow.CausalFlow[Measurement]
The causal flow associated with the current pattern.

Raises
------
FlowError
If the pattern:
- Contains measurements in forbidden planes (XZ or YZ),
- Is empty, or
- Induces a correction function and a partial order which fail the well-formedness checks for a valid causal flow.
ValueError
If ``N`` commands in the pattern do not represent a :math:`\ket{+}` state or if the pattern corrections form closed loops.

Notes
-----
This method makes use of :func:`StandardizedPattern.extract_partial_order_layers` which computes the pattern's direct acyclical graph (DAG) induced by the corrections and returns a particular layer stratification (obtained by doing a topological sort on the DAG). Further, it constructs the pattern's induced correction function from :math:`M` and :math:`X` commands.
In general, there may exist various layerings which represent the corrections of the pattern. To ensure that a given layering is compatible with the pattern's induced correction function, the partial order must be extracted from a standardized pattern. Commutation of entanglement commands with X and Z corrections in the standardization procedure may generate new corrections, which guarantees that all the topological information of the underlying graph is encoded in the extracted partial order.
"""
correction_function: dict[int, set[int]] = defaultdict(set)
pre_measured_nodes = self.results.keys() # Not included in the flow.

for m in self.m_list:
try:
bloch = m.measurement.downcast_bloch()
except TypeError:
valid = False
else:
valid = bloch.plane == Plane.XY
if not valid:
raise FlowGenericError(FlowGenericErrorReason.XYPlane)
_update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function)

for node, domain in self.x_dict.items():
_update_corrections(node, domain - pre_measured_nodes, correction_function)

og = (
self.extract_opengraph()
) # Raises a `ValueError` if `N` commands in the pattern do not represent a |+⟩ state.
partial_order_layers = (
self.extract_partial_order_layers()
) # Raises a `ValueError` if the pattern corrections form closed loops.
cf = CausalFlow(og.downcast_bloch(), dict(correction_function), partial_order_layers)
cf.check_well_formed() # Raises a `FlowError` if the partial order and the correction function are not compatible, or if a measured node is corrected by more than one node.
return cf

def extract_gflow(self) -> GFlow[BlochMeasurement]:
r"""Extract the generalized flow (gflow) structure from the current measurement pattern.

This method does not call the flow-extraction routine on the underlying open graph, but constructs the gflow from the pattern corrections instead.

Returns
-------
flow.GFlow[Measurement]
The gflow associated with the current pattern.

Raises
------
FlowError
If the pattern is empty or if the extracted structure does not satisfy
the well-formedness conditions required for a valid gflow.
TypeError
If the pattern contains a Pauli measurement
ValueError
If ``N`` commands in the pattern do not represent a :math:`\ket{+}` state or if the pattern corrections form closed loops.

Notes
-----
The notes provided in :func:`self.extract_causal_flow` apply here as well.
"""
correction_function: dict[int, set[int]] = {}
pre_measured_nodes = self.results.keys() # Not included in the flow.

for m in self.m_list:
# Raises a `TypeError` if the measurement is not represented as a Bloch measurement
if m.measurement.downcast_bloch().plane in {Plane.XZ, Plane.YZ}:
correction_function.setdefault(m.node, set()).add(m.node)
_update_corrections(m.node, m.s_domain - pre_measured_nodes, correction_function)

for node, domain in self.x_dict.items():
_update_corrections(node, domain - pre_measured_nodes, correction_function)

og = (
self.extract_opengraph()
) # Raises a `ValueError` if `N` commands in the pattern do not represent a |+⟩ state.
partial_order_layers = (
self.extract_partial_order_layers()
) # Raises a `ValueError` if the pattern corrections form closed loops.
gf = GFlow(og.downcast_bloch(), correction_function, partial_order_layers)
gf.check_well_formed()
return gf

def extract_xzcorrections(self) -> XZCorrections[Measurement]:
r"""Extract the XZ-corrections from the current measurement pattern.

Expand Down
29 changes: 15 additions & 14 deletions graphix/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,8 +963,6 @@ def extract_partial_order_layers(self) -> tuple[frozenset[int], ...]:
Notes
-----
- This function wraps :func:`optimization.StandardizedPattern.extract_partial_order_layers`, and the returned object is described in the notes of this method.

- See :func:`optimization.StandardizedPattern.extract_causal_flow` for additional information on why it is required to standardized the pattern to extract the partial order layering.
"""
return optimization.StandardizedPattern.from_pattern(self).extract_partial_order_layers()

Expand All @@ -990,11 +988,10 @@ def extract_causal_flow(self) -> CausalFlow[BlochMeasurement]:

Notes
-----
- See :func:`optimization.StandardizedPattern.extract_causal_flow` for additional information on why it is required to standardized the pattern to extract a causal flow.
- Applying the chain ``Pattern.extract_causal_flow().to_corrections().to_pattern()`` to a strongly deterministic pattern returns a new pattern implementing the same unitary transformation. This equivalence holds as long as the original pattern contains no Clifford commands, since those are discarded during open-graph extraction.
- This method requires that all the measurements in the pattern are represented as Bloch measurements (i.e., there are no :class:`PauliMeasurement`s). Use :meth:`to_bloch()` to convert all Pauli measurements.
"""
return optimization.StandardizedPattern.from_pattern(self).extract_causal_flow()
return self.extract_xzcorrections().downcast_bloch().to_causal_flow()

def extract_gflow(self) -> GFlow[BlochMeasurement]:
r"""Extract the generalized flow (gflow) structure from the current measurement pattern.
Expand All @@ -1018,7 +1015,7 @@ def extract_gflow(self) -> GFlow[BlochMeasurement]:
-----
The notes provided in :func:`self.extract_causal_flow` apply here as well.
"""
return optimization.StandardizedPattern.from_pattern(self).extract_gflow()
return self.extract_xzcorrections().downcast_bloch().to_gflow()

def extract_xzcorrections(self) -> XZCorrections[Measurement]:
"""Extract the XZ-corrections from the current measurement pattern.
Expand Down Expand Up @@ -1509,17 +1506,21 @@ def draw(
flow: PauliFlow[Measurement] | None = None

if flow_from_pattern:
pattern_std = optimization.StandardizedPattern.from_pattern(self)
try:
flow = pattern_std.extract_causal_flow()
except FlowError:
xz_corrections = self.extract_xzcorrections().downcast_bloch()
except TypeError:
pass
else:
try:
flow = pattern_std.extract_gflow()
except (FlowError, TypeError):
warn(
"The pattern is not consistent with a causal flow or a gflow. An attempt to be extract the flow from the underlying open graph will be made.",
stacklevel=stacklevel,
)
flow = xz_corrections.to_causal_flow()
except FlowError:
try:
flow = xz_corrections.to_gflow()
except FlowError:
warn(
"The pattern is not consistent with a causal flow or a gflow. An attempt to be extract the flow from the underlying open graph will be made.",
stacklevel=stacklevel,
)

if flow is None:
og = self.extract_opengraph()
Expand Down
4 changes: 2 additions & 2 deletions graphix/space_minimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ def causal_flow(pattern: StandardizedPattern) -> SpaceMinimizationHeuristicResul
This minimization heuristic is optimal but requires the pattern to have a causal flow.
"""
try:
cf = pattern.extract_causal_flow()
except FlowError:
cf = pattern.extract_xzcorrections().downcast_bloch().to_causal_flow()
except (TypeError, FlowError):
return None
else:
meas_order = tuple(chain(*reversed(cf.partial_order_layers[1:])))
Expand Down
3 changes: 3 additions & 0 deletions tests/test_pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,7 @@ class PatternFlowTestCase(NamedTuple):
# Extract causal flow from random circuits
@pytest.mark.parametrize("jumps", range(1, 11))
def test_extract_causal_flow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None:
"""Tests the round trip Pattern -> XZCorrections -> CausalFlow -> XZCorrections -> Pattern."""
rng = Generator(fx_bg.jumped(jumps))
nqubits = 2
depth = 2
Expand All @@ -1018,6 +1019,7 @@ def test_extract_causal_flow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None
# Extract gflow from random circuits
@pytest.mark.parametrize("jumps", range(1, 11))
def test_extract_gflow_rnd_circuit(self, fx_bg: PCG64, jumps: int) -> None:
"""Tests the round trip Pattern -> XZCorrections -> GFlow -> XZCorrections -> Pattern."""
rng = Generator(fx_bg.jumped(jumps))
nqubits = 2
depth = 2
Expand Down Expand Up @@ -1050,6 +1052,7 @@ def test_extract_causal_flow(self, fx_rng: Generator, test_case: PatternFlowTest

@pytest.mark.parametrize("test_case", PATTERN_FLOW_TEST_CASES)
def test_extract_gflow(self, fx_rng: Generator, test_case: PatternFlowTestCase) -> None:
"""Tests the round trip Pattern -> XZCorrections -> GFlow -> XZCorrections -> Pattern."""
if test_case.has_gflow:
alpha = 2 * np.pi * fx_rng.random()
s_ref = test_case.pattern.simulate_pattern(input_state=PlanarState(Plane.XZ, alpha), rng=fx_rng)
Expand Down
Loading