Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #445 +/- ##
==========================================
- Coverage 89.06% 88.75% -0.31%
==========================================
Files 44 46 +2
Lines 6547 6743 +196
==========================================
+ Hits 5831 5985 +154
- Misses 716 758 +42 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
graphix/circ_ext/extraction.py
Outdated
| for edge in combinations(c_set, 2): | ||
| negative_sign ^= edge in og.graph.edges() |
There was a problem hiding this comment.
The following code is a bit simpler and faster if the graph is sparse (but slower if the graph is dense; however, it would be much faster in any case if we used rustworkx!):
| for edge in combinations(c_set, 2): | |
| negative_sign ^= edge in og.graph.edges() | |
| negative_sign ^= graph.subgraph(c_set).number_of_edges() % 2 == 1 |
There was a problem hiding this comment.
Nice, thanks. Done in f99c285. Do you suggest moving to rustworks (adding this to the backlog)?
|
Hi @thierry-martinez, thanks for the review. I addressed all your comments, the corresponding modifications in the stim plugin are in commit matulni/graphix-stim-compiler@843601c. I think it's ready for another round! |
thierry-martinez
left a comment
There was a problem hiding this comment.
Some minor comments.
graphix/circ_ext/extraction.py
Outdated
| def extend_input(og: OpenGraph[Measurement]) -> tuple[OpenGraph[Measurement], dict[int, int]]: | ||
| r"""Extend the inputs of a given open graph. | ||
|
|
||
| For every input node :math:`v`, a new node :math:`u` and edge :math:`(u, v)` are added to the open graph. Node :math:`u` is measured in plane :math:`XY` with angle :math:`\alpha = 0` and replaces :math:`v` in the open graph's sequence of input nodes. |
There was a problem hiding this comment.
| For every input node :math:`v`, a new node :math:`u` and edge :math:`(u, v)` are added to the open graph. Node :math:`u` is measured in plane :math:`XY` with angle :math:`\alpha = 0` and replaces :math:`v` in the open graph's sequence of input nodes. | |
| For every input node :math:`v`, a new node :math:`u` and edge :math:`(u, v)` are added to the open graph. Node :math:`u` is measured in Pauli axis `X` and replaces :math:`v` in the open graph's sequence of input nodes. |
graphix/circ_ext/compilation.py
Outdated
| modified_qubits = [ | ||
| qubit | ||
| for qubit in range(circuit.width) | ||
| if qubit in pexp.pauli_string.x_nodes | pexp.pauli_string.y_nodes | pexp.pauli_string.z_nodes | ||
| ] |
There was a problem hiding this comment.
All tests pass with the following change:
| modified_qubits = [ | |
| qubit | |
| for qubit in range(circuit.width) | |
| if qubit in pexp.pauli_string.x_nodes | pexp.pauli_string.y_nodes | pexp.pauli_string.z_nodes | |
| ] | |
| modified_qubits = list(pexp.pauli_string.x_nodes | pexp.pauli_string.y_nodes | pexp.pauli_string.z_nodes) |
I realize we discussed this earlier and the ladder transformation output does change with this modification. However, if the order matters, should we add a test to enforce it?
There was a problem hiding this comment.
Hi @thierry-martinez, this is tricky.
From a code perspective, I think that in the current Python implementation, calling list on a set of (small?) integers always returns an ordered list:
assert [0, 1, 2] == list[{1, 0, 2}]Ultimately, I think that this is because (small?) integers are hashed with their own value:
a, b = 0, 1
assert a.__hash__() == 0
assert b.__hash__() == 1So in our case, since we are dealing with small integers (number of qubits), in both the PR's implementation and your suggestion, we get the same list of instructions (and hence all tests pass trivially).
Below, I paste a pexp_ladder_pass function where you can specify the qubit order by hand:
from itertools import pairwise, chain
from graphix.circ_ext.extraction import PauliExponential, PauliExponentialDAG
from graphix.transpiler import Circuit
from graphix.fundamentals import ANGLE_PI
def pexp_ladder_pass(pexp_dag: PauliExponentialDAG, circuit: Circuit, modified_qubits: list[int]) -> None:
r"""Add a Pauli exponential DAG to a circuit by using a ladder decomposition.
The input circuit is modified in-place. This function assumes that the Pauli exponential DAG has been remap, i.e., its Pauli strings are defined on qubit indices instead of output nodes. See :meth:`PauliString.remap` for additional information.
Parameters
----------
pexp_dag: PauliExponentialDAG
The Pauli exponential rotation to be added to the circuit. Its Pauli strings are assumed to be defined on qubit indices.
circuit : Circuit
The circuit to which the operation is added. The input circuit is assumed to be compatible with ``pexp_dag.output_nodes``.
Notes
-----
Pauli exponentials in the DAG are compiled sequentially following an arbitrary total order compatible with the DAG. Each Pauli exponential is decomposed into a sequence of basis changes, CNOT gates, and a single :math:`R_Z` rotation:
.. math::
R_Z(\phi) = \exp \left(-i \frac{\phi}{2} Z \right),
with effective angle :math:`\phi = -2\alpha`, where :math:`\alpha` is the angle encoded in `self.angle`. Basis changes map :math:`X` and :math:`Y` operators to the :math:`Z` basis before entangling the qubits in a CNOT ladder.
Gate set: H, CNOT, RZ, RY
See https://quantumcomputing.stackexchange.com/questions/5567/circuit-construction-for-hamiltonian-simulation/11373#11373 for additional information.
"""
def add_pexp(pexp: PauliExponential, circuit: Circuit) -> None:
r"""Add the Pauli exponential unitary to a quantum circuit.
This function modifies the input circuit in-place.
Parameters
----------
circuit : Circuit
The quantum circuit to which the Pauli exponential is added.
Notes
-----
It is assumed that the ``x``, ``y``, and ``z`` node sets of the Pauli string in the exponential are well-formed, i.e., contain valid qubit indices and are pairwise disjoint.
"""
if pexp.angle == 0: # No rotation
return
angle = -2 * pexp.angle * pexp.pauli_string.sign
if len(modified_qubits) == 0: # Identity
return
q0 = modified_qubits[0]
if len(modified_qubits) == 1:
if q0 in pexp.pauli_string.x_nodes:
circuit.rx(q0, angle)
elif q0 in pexp.pauli_string.y_nodes:
circuit.ry(q0, angle)
else:
circuit.rz(q0, angle)
return
add_basis_change(pexp, q0, circuit)
for q1, q2 in pairwise(modified_qubits):
add_basis_change(pexp, q2, circuit)
circuit.cnot(control=q1, target=q2)
circuit.rz(modified_qubits[-1], angle)
for q2, q1 in pairwise(modified_qubits[::-1]):
circuit.cnot(control=q1, target=q2)
add_basis_change(pexp, q2, circuit)
add_basis_change(pexp, modified_qubits[0], circuit)
def add_basis_change(pexp: PauliExponential, qubit: int, circuit: Circuit) -> None:
"""Apply an X or a Y basis change to a given qubit if required by the Pauli string.
This function modifies the input circuit in-place.
Parameters
----------
pexp : PauliExponential
The Pauli exponential under consideration.
qubit : int
The qubit on which the basis-change operation is performed.
circuit : Circuit
The quantum circuit to which the basis change is added.
"""
if qubit in pexp.pauli_string.x_nodes:
circuit.h(qubit)
elif qubit in pexp.pauli_string.y_nodes:
add_hy(qubit, circuit)
def add_hy(qubit: int, circuit: Circuit) -> None:
"""Add a pi rotation around the z + y axis.
This function modifies the input circuit in-place.
Parameters
----------
qubit : int
The qubit on which the basis-change operation is performed.
circuit : Circuit
The quantum circuit to which the basis change is added.
"""
circuit.rz(qubit, ANGLE_PI / 2)
circuit.ry(qubit, ANGLE_PI / 2)
circuit.rz(qubit, ANGLE_PI / 2)
for node in chain(*reversed(pexp_dag.partial_order_layers[1:])):
pexp = pexp_dag.pauli_exponentials[node]
add_pexp(pexp, circuit)If you compile a Pauli exponential with different modified_qubit order, you get a different circuit (with the PR's implementation or your commit you always get the same set of instructions):
from graphix.circ_ext.compilation import pexp_ladder_pass
from graphix.circ_ext.extraction import PauliString, PauliExponential, PauliExponentialDAG
from graphix.transpiler import Circuit
from graphix.sim.base_backend import NodeIndex
from graphix.fundamentals import ANGLE_PI
alpha = 0.1
pexp = PauliExponentialDAG(
pauli_exponentials={
0: PauliExponential(alpha / 2, PauliString(x_nodes={4,3}, y_nodes={1, 2})),
},
partial_order_layers=[{1, 2, 3, 4}, {0}],
output_nodes=[1, 2, 3, 4],
)
outputs_mapping = NodeIndex()
outputs_mapping.extend(pexp.output_nodes)
pexp_rmp = pexp.remap(outputs_mapping.index)
qc1 = Circuit(4)
pexp_ladder_pass_2(pexp_rmp, qc1, [0, 1, 2, 3])
qc2 = Circuit(4)
pexp_ladder_pass_2(pexp_rmp, qc2, [0, 2, 1, 3])
assert qc1.instruction != qc2.instructionBut to my surprise, they seem to represent the same unitary, up to a phase:
s1 = qc1.simulate_statevector().statevec
s2 = qc2.simulate_statevector().statevec
assert s1.isclose(s2)In summary:
- I think that Python's implementation makes it difficult to check the different between ordering the qubits because of how integers are hashed.
- I haven't managed to find an example where the order matters when we specify it by hand.
- I'm not convinced that the order does not matter in general.
There was a problem hiding this comment.
Edit: not even up to a phase:
import numpy as np
assert(np.all(np.isclose(s1.flatten(),s2.flatten())))There was a problem hiding this comment.
Ok, I convinced myself that the order on which we iterate over the qubits when synthesising the Pauli exponential does not matter.
Consider the ZZ-Pauli exponential:
The star-pass synthesis in the order
q_0: ──■─────────────■──
┌─┴─┐┌───────┐┌─┴─┐
q_1: ┤ X ├┤ Rz(α) ├┤ X ├
└───┘└───────┘└───┘
whereas the synthesis in the order
┌───┐┌───────┐┌───┐
q_0: ┤ X ├┤ Rz(α) ├┤ X ├
└─┬─┘└───────┘└─┬─┘
q_1: ──■─────────────■──
and these circuits represent the same unitary. To see that, first, note that they are equivalent up to a relabelling
This is true for bin(n). If there are
The argument does not change if we have
There was a problem hiding this comment.
Thank you very much for the explanation! Regarding the points you mentioned in the comment above, hash is indeed the identity over list(set([1,2])) == [1,2] whereas list(set([1,8])) == [8,1].
graphix/circ_ext/extraction.py
Outdated
| x_nodes: AbstractSet[int] = dataclasses.field(default_factory=frozenset) | ||
| y_nodes: AbstractSet[int] = dataclasses.field(default_factory=frozenset) | ||
| z_nodes: AbstractSet[int] = dataclasses.field(default_factory=frozenset) |
There was a problem hiding this comment.
Since x_nodes, y_nodes, and z_nodes are disjoint, I propose representing PauliString as Mapping[int, Axis], which is more canonical. See commit thierry-martinez@828f96d (and in thierry-martinez/graphix-stim-compiler@a743335 for graphix-stim-compiler).
There was a problem hiding this comment.
(By "more canonical", I mean that disjointness is structurally enforced.)
There was a problem hiding this comment.
Hi @thierry-martinez, thanks for this comment, indeed it bothered me that disjointness was not structurally enforced. I like your mapping idea, but I fear that using Axis may be a bit confusing: in Graphix this object is associated with measurements (or at least, with a notion of space), and I'm not sure if it makes much sense in this context.
What do you think about using graphix.Pauli objects ? Another advantage would be that we can explicitly map nodes to the identity (now, they are implicitly mapped by not being in the sets (or in the dictionary keys in your commit)). Including identity operators will certainly be less efficient, but all the Pauli-string processing routines will remain with the same complexity, so I think it's not a big problem (and it may improve readability). If you agree, I'll adapt your commit accordingly.
There was a problem hiding this comment.
I think that Axis expresses precisely what we want to enforce structurally (at least for the value part; keys are not required to be actual nodes). Axis is defined in fundamentals and is a subtype of IXYZ, which is a component of Pauli. As you mentioned, using IXYZ or Pauli would allow nodes to be mapped to I; therefore, we lose canonicality because mapping to I is equivalent to not being mapped. This should not be a serious problem, but the invariant that the set of keys equals precisely the nodes not mapped to I is nice to have, and defining a rotation over I by a given angle is a bit strange (it would be the identity, of course). Pauli is a pair of IXYZ and Sign, and we will not use the Sign either, which is also a loss of canonicality.
There was a problem hiding this comment.
(In other words, Pauli is a Pauli string of length 1.)
There was a problem hiding this comment.
Fair enough, I'm convinced, thanks for the comment :)
tests/test_circ_extraction.py
Outdated
|
|
||
|
|
||
| class PauliExpTestCase(NamedTuple): | ||
| p_exp: PauliExponentialDAG |
There was a problem hiding this comment.
Small nitpick - I would rename this variable pexp_dag to match the convention you use everywhere else in the codebase and to minimize the chances of it being confused with the PauliExponential object.
|
Great job on this PR, Mateo! I have one general comment regarding the assumption in several class functions in |
thierry-martinez
left a comment
There was a problem hiding this comment.
LGTM! Some minor remarks.
graphix/circ_ext/extraction.py
Outdated
| def from_focused_flow(flow: PauliFlow[Measurement]) -> CliffordMap: | ||
| """Extract a Clifford map from a focused Pauli flow. | ||
|
|
||
| This routine associates a two Pauli strings (one per generator of the Pauli group, X and Z) to each input node in ``flow.og``. |
There was a problem hiding this comment.
| This routine associates a two Pauli strings (one per generator of the Pauli group, X and Z) to each input node in ``flow.og``. | |
| This routine associates two Pauli strings (one per generator of the Pauli group, X and Z) to each input node in ``flow.og``. |
graphix/circ_ext/compilation.py
Outdated
| pexp_cp: Callable[[PauliExponentialDAG, Circuit], None] | None | ||
| Compilation pass to synthetize a Pauli exponential DAG. If ``None`` (default), :func:`pexp_ladder_pass` is employed. | ||
| cm_cp: Callable[[PauliExponentialDAG, Circuit], None] | None | ||
| Compilation pass to synthetize a Clifford map. If ``None`` (default), a `ValueError` is raised since there is still no default pass for Clifford map integrated in Graphix. |
There was a problem hiding this comment.
| Compilation pass to synthetize a Clifford map. If ``None`` (default), a `ValueError` is raised since there is still no default pass for Clifford map integrated in Graphix. | |
| Compilation pass to synthetize a Clifford map. If ``None`` (default), a ``ValueError`` is raised since there is still no default pass for Clifford map integrated in Graphix. |
graphix/circ_ext/extraction.py
Outdated
|
|
||
| Notes | ||
| ----- | ||
| The identity operator is ommitted in this representation, which means that in general it is not possible to infer the size of the Hilbert space from an instance of `PauliString` alone. |
There was a problem hiding this comment.
| The identity operator is ommitted in this representation, which means that in general it is not possible to infer the size of the Hilbert space from an instance of `PauliString` alone. | |
| The identity operator is ommitted in this representation, which means that in general it is not possible to infer the size of the Hilbert space from an instance of ``PauliString`` alone. |
graphix/circ_ext/extraction.py
Outdated
| Returns | ||
| ------- | ||
| PauliExponential | ||
| Primary extraction string associated to the input measured nodes. The sets in the returned `PauliString` instance are disjoint. |
There was a problem hiding this comment.
| Primary extraction string associated to the input measured nodes. The sets in the returned `PauliString` instance are disjoint. | |
| Primary extraction string associated to the input measured nodes. The sets in the returned ``PauliString`` instance are disjoint. |
graphix/circ_ext/extraction.py
Outdated
| pauli_exponentials: Mapping[int, PauliExponential] | ||
| Mapping between measured nodes (``keys``) and Pauli exponentials (``values``). | ||
| partial_order_layers: Sequence[AbstractSet[int]] | ||
| Partial order between the Pauli exponentials in a layer form. The set `layers[i]` comprises the nodes in layer `i`. Nodes in layer `i` are "larger" in the partial order than nodes in layer `i+1`. The pattern's output nodes are always in layer 0. |
There was a problem hiding this comment.
| Partial order between the Pauli exponentials in a layer form. The set `layers[i]` comprises the nodes in layer `i`. Nodes in layer `i` are "larger" in the partial order than nodes in layer `i+1`. The pattern's output nodes are always in layer 0. | |
| Partial order between the Pauli exponentials in a layer form. The set ``layers[i]`` comprises the nodes in layer ``i``. Nodes in layer ``i`` are "larger" in the partial order than nodes in layer ``i+1``. The pattern's output nodes are always in layer 0. |
|
@pranav97nair You make a good point: |
Co-authored-by: thierry-martinez <thierry.martinez@inria.fr>
This PR introduces functionality to perform the circuit extraction algorithm presented in Ref. [1] which allows to extract a circuit without ancillas from a strongly deterministic pattern.
Context
The main result of the paper is a procedure to perform the transformation
where$U_{\mathcal{P}}$ is the isometry implemented by the pattern $\mathcal{P}$ (a unitary when the pattern has the same number of inputs and outputs), $C$ is a Clifford map, and $V(\mathbf{\theta})$ is an ordered product of Pauli exponentials.
The structure of the extraction process reads as follows:
flowchart LR n0["Pattern"] n1["OpenGraph"] n2("Focused PauliFlow") subgraph s1["ExtractionResult"] s10("PauliExponentialDAG") s11("CliffordMap") end %% Junction node to merge arrows j1(( )) n3("Graphix circuit") n0 --> n1 n1 --> n2 n2 --> s10 n2 --> s11 %% Merging into the junction s10 --> j1 s11 --> j1 j1 --> n3 %% Styling the junction to be small style j1 fill:#000,stroke-width:0px,width:10pxThe transformation$O(N^3)$ Pauli-flow extraction algorithm in Ref. [2] always returns focused Pauli flows, contrary to the $O(N^5)$ version in Ref. [1] which requires an additional post-processing to focus the correction function.
Pattern->OpenGraph->Focused PauliFlowalready exists in the current implementation of Graphix. TheExtractionResultis the output of the algorithm which is denoted "PdDAG" in [1]. In particular:PauliExponentialDAGis a mapping between measured nodes in the open graph and Pauli exponentials, and a partial order between the measured node (the flow's partial order). Pauli exponentials are a pair of anAngle(the node's measurement angle) and aPauliStringacting on the output nodes of the open graph. The corresponding Pauli string is obtained from the focused flow's correction function.Clifford Mapdescribes a linear transformation between the space of input qubits and the space of output qubits. It is encoded as a map from the Pauli-group generators (The$Z$ -map is also obtained from the focused flow's correction function. The $X$ -map is obtained from the focused flow's correction function of the extended open graph (see Def. C.3 in [1]).
Summary of changes
Added the following to
graphix.flow.core.PauliFlow:is_focused: (method) Verifies if a Pauli flow is focused according to definition 4.3 in Ref. [1].extract_circuit: (method) Returns aExtractionResultobject.pauli_strings: (cached_property) Associates a Pauli string to every corrected node in the flow's correction function.Added new module
graphix.circ_ext.extractionFocused PauliFlow->ExtractionResult.ExtractionResultPauliStringPauliExponentialPauliExponentialDAGCliffordMapAdded new module
graphix.circ_ext.compilationExtractionResult->Graphix circuit.PauliExponentialDAG->Graphix circuitis done with the naive "star" construction (see [3]).CliffordMap->Graphix circuitrelies onstimand currently only supports unitaries (i.e., patterns with the same number of inputa and outputs). To avoid introducing a dependency onstim, theStimCliffordPassis implemented in the external plugingraphix-stim-compiler. This compilation pass together with the tooling introduced in this PR allow to do a round-trip conversion circuit -> MBQC pattern -> circuit.We note that all the transformations in the figure work with parametric objects as well.
References
[1] Simmons, 2021, arXiv:2109.05654.
[2] Mitosek and Backens, 2024, arXiv:2410.23439.
[3] https://quantumcomputing.stackexchange.com/questions/5567/circuit-construction-for-hamiltonian-simulation/11373#11373