-
Notifications
You must be signed in to change notification settings - Fork 631
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
Qualtran-pl interoperability #6921
base: master
Are you sure you want to change the base?
Changes from all commits
e9dd736
0c1caf7
f68c56d
0ca5cd0
ab573bc
025add2
68bb8ba
2e45299
d4dfd6e
e2039ed
1acb8ae
3f7d882
df5cf90
8cf199a
30e01e4
a241dc4
66ac6cd
984629d
e7f86a7
287b59a
552635c
675bca9
3bd0cd6
4ce5298
59ea823
9c128c2
f9398ac
f233754
38543ef
7a5eb2d
5a90c98
7ec5144
af0aa26
2b32ebf
9d6f390
b18e6bb
c9f67e3
2a7b23f
b1cd809
a8355a0
1937dff
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,7 @@ | ||
""" | ||
This module contains functions to load circuits from other frameworks as | ||
PennyLane templates. | ||
""" | ||
|
||
from .io import * | ||
from .qualtran_io import * |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,196 @@ | ||||||||
# 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, MatrixUndefinedError, classproperty | ||||||||
Check notice on line 31 in pennylane/io/qualtran_io.py
|
||||||||
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. | ||||||||
Comment on lines
+76
to
+77
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. Might want to use an adapter instead of a shim -
Suggested change
|
||||||||
|
||||||||
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: | ||||||||
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 | ||||||||
bloq = params[0]._hyperparameters["bloq"] | ||||||||
matrix = bloq.tensor_contract() | ||||||||
return matrix |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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): | ||
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 adding a test that compares the unitary matrices
using a non-atomic bloq b |
||
"""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 |
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.
A bit confusing but what does
bitsize=3
in this case? Do we get a SWAP on three wires?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.
The Bloq Swap basically swaps N bit registers. So in this case you would get
qml.SWAP([0, 3]), qml.SWAP([1. 4]), qml.SWAP([2, 5])
Maybe SWAP is not the best example since the qualtran Swap is different from the QML Swap.