diff --git a/qualtran/_infra/bloq.py b/qualtran/_infra/bloq.py index ec5e2d735..9e04ed867 100644 --- a/qualtran/_infra/bloq.py +++ b/qualtran/_infra/bloq.py @@ -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 @@ -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, @@ -551,3 +554,8 @@ def wire_symbol( def __str__(self): return self.__class__.__name__ + + def as_zx_gates( + 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.") diff --git a/qualtran/bloqs/basic_gates/x_basis.py b/qualtran/bloqs/basic_gates/x_basis.py index c215a4ad2..c50a28aec 100644 --- a/qualtran/bloqs/basic_gates/x_basis.py +++ b/qualtran/bloqs/basic_gates/x_basis.py @@ -17,6 +17,7 @@ import numpy as np from attrs import frozen +from numpy.typing import NDArray from qualtran import ( AddControlledT, @@ -36,6 +37,7 @@ if TYPE_CHECKING: import cirq + import pyzx as zx import quimb.tensor as qtn from qualtran.cirq_interop import CirqQuregT @@ -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} diff --git a/qualtran/bloqs/basic_gates/z_basis.py b/qualtran/bloqs/basic_gates/z_basis.py index 599acfc89..8f3cea1d8 100644 --- a/qualtran/bloqs/basic_gates/z_basis.py +++ b/qualtran/bloqs/basic_gates/z_basis.py @@ -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) @@ -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`. @@ -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: diff --git a/qualtran/pyzx_interop/__init__.py b/qualtran/pyzx_interop/__init__.py new file mode 100644 index 000000000..2e276c7e2 --- /dev/null +++ b/qualtran/pyzx_interop/__init__.py @@ -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 diff --git a/qualtran/pyzx_interop/bloq_to_pyzx_circuit.py b/qualtran/pyzx_interop/bloq_to_pyzx_circuit.py new file mode 100644 index 000000000..611f7f915 --- /dev/null +++ b/qualtran/pyzx_interop/bloq_to_pyzx_circuit.py @@ -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 +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 + + cbloq = bloq.decompose_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 diff --git a/qualtran/pyzx_interop/bloq_to_pyzx_circuit_test.py b/qualtran/pyzx_interop/bloq_to_pyzx_circuit_test.py new file mode 100644 index 000000000..856c5a1a6 --- /dev/null +++ b/qualtran/pyzx_interop/bloq_to_pyzx_circuit_test.py @@ -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) diff --git a/qualtran/simulation/tensor/_quimb.py b/qualtran/simulation/tensor/_quimb.py index d4957e477..c626b389c 100644 --- a/qualtran/simulation/tensor/_quimb.py +++ b/qualtran/simulation/tensor/_quimb.py @@ -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) tn.add( qtn.Tensor( data=np.eye(2),