Skip to content

PyZX Interop: Converting bloqs to pyzx circuits #1550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
8 changes: 8 additions & 0 deletions qualtran/_infra/bloq.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
if TYPE_CHECKING:
import cirq
import networkx as nx
import numpy as np
import pyzx as zx
import quimb.tensor as qtn
import sympy
from numpy.typing import NDArray
Expand All @@ -39,6 +41,7 @@
from qualtran.cirq_interop import CirqQuregT
from qualtran.cirq_interop.t_complexity_protocol import TComplexity
from qualtran.drawing import WireSymbol
from qualtran.pyzx_interop import ZXAncillaManager
from qualtran.resource_counting import (
BloqCountDictT,
BloqCountT,
Expand Down Expand Up @@ -551,3 +554,8 @@ def wire_symbol(

def __str__(self):
return self.__class__.__name__

def as_zx_gates(
Copy link
Collaborator

Choose a reason for hiding this comment

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

this need a big docstring (as all top level bloq methods)

self, ancilla_manager: 'ZXAncillaManager', /, **qubits: 'NDArray[np.integer]'
) -> tuple[list['zx.circuit.Gate'], dict[str, 'NDArray[np.integer]']]:
raise NotImplementedError(f"{self} does not declare a conversion to ZX gates.")
10 changes: 10 additions & 0 deletions qualtran/bloqs/basic_gates/x_basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import numpy as np
from attrs import frozen
from numpy.typing import NDArray

from qualtran import (
AddControlledT,
Expand All @@ -36,6 +37,7 @@

if TYPE_CHECKING:
import cirq
import pyzx as zx
import quimb.tensor as qtn

from qualtran.cirq_interop import CirqQuregT
Expand Down Expand Up @@ -263,3 +265,11 @@ def wire_symbol(self, reg: Register, idx: Tuple[int, ...] = tuple()) -> 'WireSym
return Text('X')

return ModPlus()

def as_zx_gates(
self, ancilla_manager, /, q: NDArray[np.integer]
) -> tuple[list['zx.circuit.Gate'], dict[str, NDArray[np.integer]]]:
import pyzx as zx

(qubit,) = q
return [zx.circuit.NOT(qubit)], {'q': q}
32 changes: 32 additions & 0 deletions qualtran/bloqs/basic_gates/z_basis.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@

if TYPE_CHECKING:
import cirq
import pyzx as zx
import quimb.tensor as qtn

from qualtran.cirq_interop import CirqQuregT
from qualtran.pyzx_interop import ZXAncillaManager
from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator

_ZERO = np.array([1, 0], dtype=np.complex128)
Expand Down Expand Up @@ -137,6 +139,28 @@ def wire_symbol(
s = '1' if self.bit else '0'
return directional_text_box(s, side=reg.side)

def as_zx_gates(
self, ancilla_manager: 'ZXAncillaManager', /, **qubits: NDArray[np.integer]
) -> tuple[list['zx.circuit.Gate'], dict[str, NDArray[np.integer]]]:
import pyzx as zx

if self.state:
qubit = ancilla_manager.allocate()

gates = [zx.circuit.gates.InitAncilla(qubit), zx.circuit.gates.HAD(qubit)]
if self.bit:
gates = gates + [zx.circuit.gates.NOT(qubit)]

return gates, {'q': np.array([qubit])}
else:
(qubit,) = qubits.pop('q')

gates = [zx.circuit.gates.HAD(qubit), zx.circuit.gates.PostSelect(qubit)]
if self.bit:
gates = [zx.circuit.gates.NOT(qubit)] + gates

return gates, {}


def _hide_base_fields(cls, fields):
# for use in attrs `field_transformer`.
Expand Down Expand Up @@ -285,6 +309,14 @@ def wire_symbol(

return TextBox('Z')

def as_zx_gates(
self, ancilla_manager, /, q: NDArray[np.integer]
) -> tuple[list['zx.circuit.Gate'], dict[str, NDArray[np.integer]]]:
import pyzx as zx

(qubit,) = q
return [zx.circuit.Z(qubit)], {'q': q}


@bloq_example
def _zgate() -> ZGate:
Expand Down
14 changes: 14 additions & 0 deletions qualtran/pyzx_interop/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .bloq_to_pyzx_circuit import bloq_to_pyzx_circuit, ZXAncillaManager
179 changes: 179 additions & 0 deletions qualtran/pyzx_interop/bloq_to_pyzx_circuit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Iterable

import numpy as np
import pyzx as zx
from attrs import define
from numpy.typing import NDArray

from qualtran import Bloq, CompositeBloq, LeftDangle, Register, RightDangle, Signature, Soquet

ZXQubitMap = dict[str, NDArray[np.integer]]
"""A mapping from register names to an NDArray of ZX qubits"""


@define
Copy link
Collaborator

Choose a reason for hiding this comment

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

can this be frozen? if yes: make it so; if no, use attrs.mutable and explain why in docstring

class ZXAncillaManager:
"""A simple ancilla qubit manager for generating pyzx Circuits.

Attributes:
n: number of existing qubits in the starting circuit.
"""

n: int

def allocate(self) -> int:
"""Allocate an uninitialized ancilla qubit.

This returns the index of a new ancilla qubit for use.
Note that it must be manually initialized, e.g. using the `InitAncilla` gate.
"""
idx = self.n
self.n += 1
return idx

def free(self, q: int):
"""Free an ancilla qubit.

Discard an ancilla qubit. For now, this operation does nothing.
"""


def _empty_qubit_map_from_registers(registers: Iterable[Register]) -> ZXQubitMap:
"""For each register, creates an empty NDArray of the appropriate shape to store the zx qubits."""
return {reg.name: np.empty(reg.shape + (reg.bitsize,), dtype=int) for reg in registers}


def _initalize_zx_circuit_from_signature(
signature: Signature,
) -> tuple[zx.Circuit, ZXQubitMap, ZXAncillaManager]:
"""Initalize a pyzx circuit from a bloq signature.

This enumerates the qubits in the same order as the registers in the signature.

Args:
signature: the signature of the bloq.

Returns:
A tuple of the pyzx circuit, the mapping from register names to qubits, and an
ancilla manager for the circuit.
"""

n_qubits: int = 0
qubit_d: ZXQubitMap = {}

for reg in signature.lefts():
n = reg.total_bits()
idxs = np.arange(n) + n_qubits
shape = reg.shape + (reg.bitsize,)
qubit_d[reg.name] = idxs.reshape(shape)
n_qubits += n

circ = zx.Circuit(qubit_amount=n_qubits)
return circ, qubit_d, ZXAncillaManager(n_qubits)


def _add_bloq_to_pyzx_circuit(
circ: zx.Circuit, bloq: Bloq, ancilla_manager: ZXAncillaManager, in_qubits: ZXQubitMap
) -> ZXQubitMap:
"""Add a single bloq acting on the given input qubits to a pyzx circuit.

Args:
circ: the pyzx circuit.
bloq: the bloq to add.
ancilla_manager: the ancilla manager for `circ`.
in_qubits: the input qubits to the bloq.

Returns:
A mapping of output register names to output qubits.
"""
try:
gates, out_qubits = bloq.as_zx_gates(ancilla_manager, **in_qubits)
for gate in gates:
circ.add_gate(gate)
return out_qubits
except NotImplementedError:
pass
Comment on lines +107 to +108
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is so we fall back to the decomposition-based approach? Maybe some line comments could help guide the reader to the order of strategies


cbloq = bloq.decompose_bloq()
Copy link
Collaborator

Choose a reason for hiding this comment

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

consider supporting the case where bloq is a composite bloq

return _add_cbloq_to_pyzx_circuit(circ, cbloq, ancilla_manager, in_qubits)


def _add_cbloq_to_pyzx_circuit(
circ: zx.Circuit, cbloq: CompositeBloq, ancilla_manager: ZXAncillaManager, in_qubits: ZXQubitMap
) -> ZXQubitMap:
"""Add a composite bloq acting on the given input qubits to a pyzx circuit.

This iterates through the `cbloq` graph in topologically-sorted order, and adds each
bloq instance.

Args:
circ: the pyzx circuit.
cbloq: the composite bloq to add.
ancilla_manager: the ancilla manager for `circ`.
in_qubits: the input qubits to the bloq.

Returns:
A mapping of output register names to output qubits.
"""
# initialize the soquets corresponding to the `cbloq` inputs
soq_map: dict[Soquet, NDArray[np.integer]] = {
soq: in_qubits[soq.reg.name][soq.idx]
for soq in cbloq.all_soquets
if soq.binst is LeftDangle
}

for binst, pred_cxns, succ_cxns in cbloq.iter_bloqnections():
bloq = binst.bloq

# compute the input qubits
bloq_in_qubits: ZXQubitMap = _empty_qubit_map_from_registers(bloq.signature.lefts())
for cxn in pred_cxns:
bloq_soq = cxn.right
bloq_in_qubits[bloq_soq.reg.name][bloq_soq.idx] = soq_map.pop(cxn.left)

out_qubits: ZXQubitMap = _add_bloq_to_pyzx_circuit(
circ, bloq, ancilla_manager, bloq_in_qubits
)

# forward the output qubits to their corresponding soqs
for cxn in succ_cxns:
bloq_soq = cxn.left
soq_map[bloq_soq] = out_qubits[bloq_soq.reg.name][bloq_soq.idx]

# forward the soqs to the cbloq output soqs
for cxn in cbloq.connections:
if cxn.right.binst is RightDangle:
soq_map[cxn.right] = soq_map.pop(cxn.left)

# get the output qubits
out_qubits = _empty_qubit_map_from_registers(cbloq.signature.rights())
for soq, qubits in soq_map.items():
out_qubits[soq.reg.name][soq.idx] = qubits
return out_qubits


def bloq_to_pyzx_circuit(bloq: Bloq) -> zx.Circuit:
"""Build a pyzx circuit of a bloq.

Args:
bloq: the bloq to convert.

Returns:
A pyzx circuit corresponding to `bloq`.
"""
circ, in_qubits, ancilla_manager = _initalize_zx_circuit_from_signature(bloq.signature)
_ = _add_bloq_to_pyzx_circuit(circ, bloq, ancilla_manager, in_qubits)
return circ
44 changes: 44 additions & 0 deletions qualtran/pyzx_interop/bloq_to_pyzx_circuit_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
import pyzx as zx

from qualtran import Bloq, BloqBuilder, Signature, Soquet, SoquetT
from qualtran.bloqs.basic_gates import OneEffect, XGate, ZeroState, ZGate
from qualtran.pyzx_interop.bloq_to_pyzx_circuit import bloq_to_pyzx_circuit


class TestBloq(Bloq):
@property
def signature(self) -> 'Signature':
return Signature.build(q=1)

def build_composite_bloq(self, bb: 'BloqBuilder', q: 'Soquet') -> dict[str, 'SoquetT']:
q = bb.add(ZGate(), q=q)
q = bb.add(XGate(), q=q)

a = bb.add(ZeroState())
a = bb.add(XGate(), q=a)
bb.add(OneEffect(), q=a)

return {'q': q}


def test_bloq_to_pyzx_circuit():
bloq = TestBloq()
circ = bloq_to_pyzx_circuit(bloq)
tensor = bloq.tensor_contract()

assert zx.compare_tensors(circ, tensor)
np.testing.assert_allclose(np.imag(zx.find_scalar_correction(circ, tensor)), 0, atol=1e-7)
3 changes: 0 additions & 3 deletions qualtran/simulation/tensor/_quimb.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,8 @@ def cbloq_to_quimb(cbloq: CompositeBloq) -> qtn.TensorNetwork:
# the tensor network. Add an identity tensor acting on this register to make sure the
# tensor network has variables corresponding to all input / output registers.

n = cxn.left.reg.bitsize
for j in range(cxn.left.reg.bitsize):

placeholder = Soquet(None, Register('simulation_placeholder', QBit())) # type: ignore
Connection(cxn.left, placeholder)
Copy link
Collaborator

Choose a reason for hiding this comment

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

lol

tn.add(
qtn.Tensor(
data=np.eye(2),
Expand Down