-
Notifications
You must be signed in to change notification settings - Fork 59
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
base: main
Are you sure you want to change the base?
Changes from all commits
b53293c
36c7b8e
52ccf67
8c57642
562323d
4169c14
9e6eae2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can this be frozen? if yes: make it so; if no, use |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. consider supporting the case where |
||
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 |
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lol |
||
tn.add( | ||
qtn.Tensor( | ||
data=np.eye(2), | ||
|
There was a problem hiding this comment.
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)