Skip to content
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

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e9dd736
initial draft
austingmhuang Feb 3, 2025
0c1caf7
prototype
austingmhuang Feb 3, 2025
f68c56d
edge case fix
austingmhuang Feb 5, 2025
0ca5cd0
hacky way to work for atomic gates
austingmhuang Feb 6, 2025
ab573bc
Merge branch 'master' into qualtran_pl
Jaybsoni Feb 6, 2025
025add2
fix
austingmhuang Feb 6, 2025
68bb8ba
small things
austingmhuang Feb 6, 2025
2e45299
black
austingmhuang Feb 6, 2025
d4dfd6e
some comments
austingmhuang Feb 6, 2025
e2039ed
add another todo
austingmhuang Feb 6, 2025
1acb8ae
Merge branch 'master' into qualtran_pl
austingmhuang Feb 7, 2025
3f7d882
flatten is not ideal
austingmhuang Feb 11, 2025
df5cf90
ask about recursive solution in cirq
austingmhuang Feb 11, 2025
8cf199a
small fix
austingmhuang Feb 11, 2025
30e01e4
use as_pl_op
austingmhuang Feb 18, 2025
a241dc4
temporary
austingmhuang Feb 20, 2025
66ac6cd
ready for rev
austingmhuang Feb 20, 2025
984629d
codefactor
austingmhuang Feb 20, 2025
e7f86a7
codefactor
austingmhuang Feb 20, 2025
287b59a
Merge branch 'master' into qualtran_pl
austingmhuang Feb 21, 2025
552635c
tests
austingmhuang Feb 21, 2025
675bca9
small tests
austingmhuang Feb 21, 2025
3bd0cd6
yay
austingmhuang Feb 21, 2025
4ce5298
black
austingmhuang Feb 21, 2025
59ea823
Merge branch 'master' into qualtran_pl
austingmhuang Feb 24, 2025
9c128c2
repr test
austingmhuang Feb 24, 2025
f9398ac
fix bug
austingmhuang Feb 24, 2025
f233754
helper function
austingmhuang Feb 24, 2025
38543ef
example
austingmhuang Feb 25, 2025
7a5eb2d
black
austingmhuang Feb 25, 2025
5a90c98
black/pylint
austingmhuang Feb 25, 2025
7ec5144
black/pylint
austingmhuang Feb 25, 2025
af0aa26
Merge branch 'master' into qualtran_pl
austingmhuang Feb 25, 2025
2b32ebf
one more test
austingmhuang Feb 25, 2025
9d6f390
one more test
austingmhuang Feb 25, 2025
b18e6bb
one more test
austingmhuang Feb 25, 2025
c9f67e3
temporary fix
austingmhuang Feb 25, 2025
2a7b23f
temporary fix
austingmhuang Feb 25, 2025
b1cd809
one more test
austingmhuang Feb 25, 2025
a8355a0
one more complicated test
austingmhuang Feb 25, 2025
1937dff
fix to handle allocate
austingmhuang Feb 27, 2025
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
7 changes: 7 additions & 0 deletions pennylane/io/__init__.py
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 *
File renamed without changes.
196 changes: 196 additions & 0 deletions pennylane/io/qualtran_io.py
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

View check run for this annotation

codefactor.io / CodeFactor

pennylane/io/qualtran_io.py#L31

Unused MatrixUndefinedError imported from pennylane.operation (unused-import)

Check notice on line 31 in pennylane/io/qualtran_io.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane/io/qualtran_io.py#L31

Unused classproperty imported from pennylane.operation (unused-import)
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])}
Comment on lines +52 to +53
Copy link
Contributor

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?

Copy link
Contributor Author

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.

"""

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Might want to use an adapter instead of a shim -

Suggested change
r"""
A shim for using bloqs as a PennyLane operation.
r"""An adapter for using Qualtran [bloqs](https://qualtran.readthedocs.io/en/latest/bloqs/index.html) 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}

Check notice on line 112 in pennylane/io/qualtran_io.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane/io/qualtran_io.py#L112

Access to a protected member _hyperparameters of a client class (protected-access)
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:

Check notice on line 187 in pennylane/io/qualtran_io.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane/io/qualtran_io.py#L187

Parameter 'cls' has been renamed to 'self' in overriding 'FromBloq.has_matrix' method (arguments-renamed)
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
2 changes: 1 addition & 1 deletion pennylane/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -817,6 +816,7 @@ def matrix(self, wire_order: Optional[WiresLike] = None) -> TensorLike:
and set(self.wires) == set(wire_order)
)
):
print("here")
return canonical_matrix

return expand_matrix(canonical_matrix, wires=self.wires, wire_order=wire_order)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_io.py → tests/io/test_io.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
169 changes: 169 additions & 0 deletions tests/io/test_qualtran_io.py
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):

Choose a reason for hiding this comment

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

consider adding a test that compares the unitary matrices

  • derived using Bloq.tensor_contract()
  • derived using FromBloq(b).matrix()
  • reference

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
Loading