diff --git a/pennylane/io/__init__.py b/pennylane/io/__init__.py new file mode 100644 index 00000000000..1dd8268e160 --- /dev/null +++ b/pennylane/io/__init__.py @@ -0,0 +1,7 @@ +""" +This module contains functions to load circuits from other frameworks as +PennyLane templates. +""" + +from .io import * +from .qualtran_io import * diff --git a/pennylane/io.py b/pennylane/io/io.py similarity index 100% rename from pennylane/io.py rename to pennylane/io/io.py diff --git a/pennylane/io/qualtran_io.py b/pennylane/io/qualtran_io.py new file mode 100644 index 00000000000..0044907ea7c --- /dev/null +++ b/pennylane/io/qualtran_io.py @@ -0,0 +1,200 @@ +# Copyright 2018-2025 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" +This submodule contains the adapter class for Qualtran-PennyLane interoperability. +""" +# pylint:disable= +from qualtran import ( + Bloq, + CompositeBloq, + Soquet, + LeftDangle, + Side, + DecomposeNotImplementedError, + DecomposeTypeError, +) + +import numpy as np +import pennylane as qml + +from pennylane.operation import Operation +from pennylane.wires import WiresLike + + +def get_bloq_registers_info(bloq): + """Returns a `qml.registers` object associated with all named and unnamed registers and wires + in the bloq. + + Args: + bloq: the bloq to get the registers info of + + Returns: + dict: A dictionary that has all the named and un-named registers with default wire + ordering. + + **Example** + + Given a qualtran bloq: + + from qualtran.bloqs.basic_gates import Swap + + >>> qml.get_bloq_registers_info(Swap(3)) + {'x': Wires([0, 1, 2]), 'y': Wires([3, 4, 5])} + """ + + cbloq = bloq.decompose_bloq() if not isinstance(bloq, CompositeBloq) else bloq + + temp_register_dict = {} + for reg in cbloq.signature.rights(): + temp_register_dict[reg.name] = reg.bitsize + + return qml.registers(temp_register_dict) + + +def _get_named_registers(registers): + """Returns a `qml.registers` object associated with the named registers in the bloq""" + + temp_register_dict = {} + for reg in registers: + temp_register_dict[reg.name] = reg.bitsize + + return qml.registers(temp_register_dict) + + +class FromBloq(Operation): + r""" + A shim for using bloqs as a PennyLane operation. + + Args: + bloq: the bloq to wrap + wires: the wires to act on + + **Example** + + Given a qualtran bloq: + + from qualtran.bloqs.basic_gates import CNOT + + >>> qualtran_toffoli = qml.FromBloq(CNOT(), [0, 1]) + >>> qualtran_toffoli.matrix() + array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], + [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j], + [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j], + [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j]]) + + A simple example showcasing how to use `qml.FromBloq` inside a device: + + .. code-block:: + + dev = qml.device("default.qubit") + + @qml.qnode(dev) + def circuit(): + qml.FromBloq(XGate(), [0]) + return qml.expval(qml.Z(wires=[0])) + + >>> circuit() + -1.0 + """ + + def __init__(self, bloq: Bloq, wires: WiresLike): + self._hyperparameters = {"bloq": bloq} + super().__init__(wires=wires, id=None) + + def __repr__(self): # pylint: disable=protected-access + return f'FromBloq({self._hyperparameters["bloq"]}, wires={self.wires})' + + def compute_decomposition( + self, wires, **kwargs + ): # pylint: disable=arguments-differ, unused-argument + ops = [] + bloq = self._hyperparameters["bloq"] + + try: + cbloq = bloq.decompose_bloq() if not isinstance(bloq, CompositeBloq) else bloq + temp_registers = _get_named_registers(cbloq.signature.lefts()) + soq_to_wires = { + Soquet(LeftDangle, idx=idx, reg=reg): list(temp_registers[reg.name]) + for reg in cbloq.signature.lefts() + for idx in reg.all_idxs() + } + + for binst, pred_cxns, succ_cxns in cbloq.iter_bloqnections(): + in_quregs = { + reg.name: np.empty((*reg.shape, reg.bitsize), dtype=object).flatten() + for reg in binst.bloq.signature.lefts() + } + out_quregs = { + reg.name: np.empty((*reg.shape, reg.bitsize), dtype=object).flatten() + for reg in binst.bloq.signature.rights() + } + + soq_to_wires_len = 0 + if len(soq_to_wires.values()) > 0: + try: + soq_to_wires_len = list(soq_to_wires.values())[-1][-1] + 1 + except (IndexError, TypeError): + soq_to_wires_len = list(soq_to_wires.values())[-1] + 1 + + for pred in pred_cxns: + soq = pred.right + soq_to_wires[soq] = soq_to_wires[pred.left] + in_quregs[soq.reg.name][soq.idx] = soq_to_wires[soq] + + for succ in succ_cxns: + soq = succ.left + if soq.reg.side == Side.RIGHT: + # If in_quregs is not equal to out_quregs, we insert key, value pair where the key is + # the register name, and the value is the list of wires associated with it + if len(in_quregs) != len(out_quregs) and soq.reg.side == Side.RIGHT: + total_elements = np.prod(soq.reg.shape) * soq.reg.bitsize + ascending_vals = np.arange( + soq_to_wires_len, + total_elements + soq_to_wires_len, + dtype=object, + ) + in_quregs[soq.reg.name] = ascending_vals.reshape( + (*soq.reg.shape, soq.reg.bitsize) + ) + soq_to_wires[soq] = in_quregs[soq.reg.name][soq.idx] + + total_wires = [w for ws in in_quregs.values() for w in list(ws.flatten())] + + mapped_wires = [] + for idx in total_wires: + mapped_wires.append(wires[idx]) + op = binst.bloq.as_pl_op(mapped_wires) + + if op: + ops.append(op) + except (DecomposeNotImplementedError, DecomposeTypeError): + pass + + return ops + + @property + def has_matrix( + self, + ) -> bool: # pylint: disable=invalid-overridden-method, protected-access + r"""Return if the bloq has a valid matrix representation.""" + bloq = self._hyperparameters["bloq"] + matrix = bloq.tensor_contract() + return matrix.shape == (2 ** len(self.wires), 2 ** len(self.wires)) + + def compute_matrix( + *params, **kwargs + ): # pylint: disable=unused-argument, no-self-argument, no-method-argument, protected-access + bloq = params[0]._hyperparameters["bloq"] + matrix = bloq.tensor_contract() + return matrix diff --git a/pennylane/operation.py b/pennylane/operation.py index 2bab2b7d19d..721c609347f 100644 --- a/pennylane/operation.py +++ b/pennylane/operation.py @@ -808,7 +808,6 @@ def matrix(self, wire_order: Optional[WiresLike] = None) -> TensorLike: tensor_like: matrix representation """ canonical_matrix = self.compute_matrix(*self.parameters, **self.hyperparameters) - if ( wire_order is None or self.wires == Wires(wire_order) diff --git a/tests/test_io.py b/tests/io/test_io.py similarity index 99% rename from tests/test_io.py rename to tests/io/test_io.py index 2575bfcf66d..f132b1cfd10 100644 --- a/tests/test_io.py +++ b/tests/io/test_io.py @@ -1,4 +1,4 @@ -# Copyright 2018-2020 Xanadu Quantum Technologies Inc. +# Copyright 2018-2025 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/tests/io/test_qualtran_io.py b/tests/io/test_qualtran_io.py new file mode 100644 index 00000000000..4496b42b8c2 --- /dev/null +++ b/tests/io/test_qualtran_io.py @@ -0,0 +1,169 @@ +# Copyright 2018-2025 Xanadu Quantum Technologies Inc. + +# 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 + +# http://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. +""" +Unit tests for the :mod:`pennylane.io.qualtran_io` module. +""" +import numpy as np +import pennylane as qml + + +class TestFromBloq: + """Test that FromBloq accurately wraps around Bloqs.""" + + def test_repr(self): + """Tests that FromBloq has the correct __repr__""" + + from qualtran.bloqs.basic_gates import XGate + + assert qml.FromBloq(XGate(), 1).__repr__() == "FromBloq(XGate, wires=Wires([1]))" + + def test_composite_bloq_advanced(self): + """Tests that a composite bloq with higher level abstract bloqs has the correct + decomposition after wrapped with `FromBloq`""" + from qualtran import BloqBuilder + from qualtran import QUInt + from qualtran.bloqs.arithmetic import Product, Add + from pennylane.wires import Wires + + bb = BloqBuilder() + + w1 = bb.add_register("p1", 3) + w2 = bb.add_register("p2", 3) + w3 = bb.add_register("q1", 3) + w4 = bb.add_register("q2", 3) + + w1, w2, res1 = bb.add(Product(3, 3), a=w1, b=w2) + w3, w4, res2 = bb.add(Product(3, 3), a=w3, b=w4) + p1p2, p1p2_plus_q1q2 = bb.add(Add(QUInt(bitsize=6), QUInt(bitsize=6)), a=res1, b=res2) + + cbloq = bb.finalize(p1=w1, p2=w2, q1=w3, q2=w4, p1p2=p1p2, p1p2_plus_q1q2=p1p2_plus_q1q2) + + expected = [ + qml.FromBloq(Product(3, 3), wires=Wires([0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17])), + qml.FromBloq(Product(3, 3), wires=Wires([6, 7, 8, 9, 10, 11, 18, 19, 20, 21, 22, 23])), + qml.FromBloq( + Add(QUInt(bitsize=6), QUInt(bitsize=6)), + wires=Wires([12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]), + ), + ] + assert qml.FromBloq(cbloq, wires=range(24)).decomposition() == expected + + def test_composite_bloq(self): + """Tests that a simple composite bloq has the correct decomposition after wrapped with `FromBloq`""" + from qualtran import BloqBuilder + from qualtran.bloqs.basic_gates import Hadamard, CNOT, Toffoli + + bb = BloqBuilder() # bb is the circuit like object + + w1 = bb.add_register("wire1", 1) + w2 = bb.add_register("wire2", 1) + aux = bb.add_register("aux_wires", 2) + + aux_wires = bb.split(aux) + + w1 = bb.add(Hadamard(), q=w1) + w2 = bb.add(Hadamard(), q=w2) + + w1, aux1 = bb.add(CNOT(), ctrl=w1, target=aux_wires[0]) + w2, aux2 = bb.add(CNOT(), ctrl=w2, target=aux_wires[1]) + + ctrl_aux, w1 = bb.add(Toffoli(), ctrl=(aux1, aux2), target=w1) + ctrl_aux, w2 = bb.add(Toffoli(), ctrl=ctrl_aux, target=w2) + aux_wires = bb.join(ctrl_aux) + + circuit_bloq = bb.finalize(wire1=w1, wire2=w2, aux_wires=aux_wires) + + decomp = qml.FromBloq(circuit_bloq, wires=list(range(4))).decomposition() + expected_decomp = [ + qml.H(0), + qml.H(1), + qml.CNOT([0, 2]), + qml.CNOT([1, 3]), + qml.Toffoli([2, 3, 0]), + qml.Toffoli([2, 3, 1]), + ] + assert decomp == expected_decomp + + mapped_decomp = qml.FromBloq(circuit_bloq, wires=[3, 0, 1, 2]).decomposition() + mapped_expected_decomp = [ + qml.H(3), + qml.H(0), + qml.CNOT([3, 1]), + qml.CNOT([0, 2]), + qml.Toffoli([1, 2, 3]), + qml.Toffoli([1, 2, 0]), + ] + assert mapped_decomp == mapped_expected_decomp + + def test_atomic_bloqs(self): + """Tests that atomic bloqs have the correct PennyLane equivalent after wrapped with `FromBloq`""" + from qualtran.bloqs.basic_gates import Hadamard, CNOT, Toffoli + + assert Hadamard().as_pl_op(0) == qml.Hadamard(0) + assert CNOT().as_pl_op([0, 1]) == qml.CNOT([0, 1]) + assert Toffoli().as_pl_op([0, 1, 2]) == qml.Toffoli([0, 1, 2]) + + assert np.allclose(qml.FromBloq(Hadamard(), 0).matrix(), qml.Hadamard(0).matrix()) + assert np.allclose(qml.FromBloq(CNOT(), [0, 1]).matrix(), qml.CNOT([0, 1]).matrix()) + assert np.allclose( + qml.FromBloq(Toffoli(), [0, 1, 2]).matrix(), qml.Toffoli([0, 1, 2]).matrix() + ) + + def test_bloqs(self): + """Tests that bloqs with decompositions have the correct PennyLane decompositions after + being wrapped with `FromBloq`""" + + from qualtran.bloqs.basic_gates import Swap + + assert qml.FromBloq(Swap(3), wires=range(6)).decomposition() == [ + qml.SWAP(wires=[0, 3]), + qml.SWAP(wires=[1, 4]), + qml.SWAP(wires=[2, 5]), + ] + + def test_get_bloq_registers_info(self): + """Tests that get_bloq_registers_info returns the expected dictionary with the correct + registers and wires.""" + + from qualtran import BloqBuilder + from qualtran import QUInt + from qualtran.bloqs.arithmetic import Product, Add + from pennylane.wires import Wires + + bb = BloqBuilder() + + w1 = bb.add_register("p1", 3) + w2 = bb.add_register("p2", 3) + w3 = bb.add_register("q1", 3) + w4 = bb.add_register("q2", 3) + + w1, w2, res1 = bb.add(Product(3, 3), a=w1, b=w2) + w3, w4, res2 = bb.add(Product(3, 3), a=w3, b=w4) + p1p2, p1p2_plus_q1q2 = bb.add(Add(QUInt(bitsize=6), QUInt(bitsize=6)), a=res1, b=res2) + + circuit_bloq = bb.finalize( + p1=w1, p2=w2, q1=w3, q2=w4, p1p2=p1p2, p1p2_plus_q1q2=p1p2_plus_q1q2 + ) + + expected = { + "p1": Wires([0, 1, 2]), + "p2": Wires([3, 4, 5]), + "q1": Wires([6, 7, 8]), + "q2": Wires([9, 10, 11]), + "p1p2": Wires([12, 13, 14, 15, 16, 17]), + "p1p2_plus_q1q2": Wires([18, 19, 20, 21, 22, 23]), + } + actual = qml.get_bloq_registers_info(circuit_bloq) + + assert actual == expected