Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
151 changes: 151 additions & 0 deletions qualtran/quirk_interop/bloq_to_quirk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import subprocess
from typing import Optional

from qualtran import Bloq, DecomposeTypeError, CompositeBloq
from qualtran.bloqs.bookkeeping import Join, Split
from qualtran.drawing import (
ModPlus,
Circle,
LarrowTextBox,
RarrowTextBox,
LineManager,
get_musical_score_data,
)
from qualtran.drawing.musical_score import _cbloq_musical_score


class SparseLineManager(LineManager):
"""
LineManager which keeps partitioned line slots reserved for them until they need it again

# DIDN'TDO: only handles partition patterns of the type (QAny(n)/QUInt(n)/... -> QBit((n,))
# or QBit((n,)) -> QAny(n))
"""

def __init__(self, cbloq: CompositeBloq, max_n_lines: int = 100):
super().__init__(max_n_lines)
# Pre-layout pass with a plain LineManager, used only to infer Join/Split pairing.
_, self.soq_assign, _ = _cbloq_musical_score(
cbloq.signature, binst_graph=cbloq._binst_graph, manager=LineManager()
)
self._join_to_split_id = self._build_join_to_split_map()
self._split_to_join_id = self._build_split_to_join_map()

def _find_dual_on_line(self, line: int, start: int, dual_cls):
dual_candidates = [
(rpos.seq_x, soq.binst.i) # type: ignore[union-attr]
for soq, rpos in self.soq_assign.items()
if rpos.y == line and rpos.seq_x > start and soq.binst.bloq_is(dual_cls)
]
Comment on lines +35 to +39

Choose a reason for hiding this comment

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

critical

The soq.binst attribute can be a DanglingT instance, which does not have bloq_is or i attributes. Accessing them will lead to a runtime AttributeError. You should add a check to ensure soq.binst is a BloqInstance before accessing these attributes.

Suggested change
dual_candidates = [
(rpos.seq_x, soq.binst.i) # type: ignore[union-attr]
for soq, rpos in self.soq_assign.items()
if rpos.y == line and rpos.seq_x > start and soq.binst.bloq_is(dual_cls)
]
from qualtran import BloqInstance
dual_candidates = [
(rpos.seq_x, soq.binst.i)
for soq, rpos in self.soq_assign.items()
if isinstance(soq.binst, BloqInstance)
and rpos.y == line
and rpos.seq_x > start
and soq.binst.bloq_is(dual_cls)
]

if not dual_candidates:
return None
dual_candidates.sort(key=lambda x: x[0])
return dual_candidates[0][1]

def _build_join_to_split_map(self):
join_to_split = {}
for soq, rpos in self.soq_assign.items():
if soq.binst.bloq_is(Join) and soq.idx == ():
dual_id = self._find_dual_on_line(rpos.y, rpos.seq_x, Split)
if dual_id is not None:
join_to_split[soq.binst.i] = dual_id # type: ignore[union-attr]
return join_to_split
Comment on lines +46 to +52

Choose a reason for hiding this comment

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

critical

The soq.binst attribute can be a DanglingT instance, which does not have bloq_is or i attributes. Accessing them will lead to a runtime AttributeError. You should add a check to ensure soq.binst is a BloqInstance before accessing these attributes.

Suggested change
join_to_split = {}
for soq, rpos in self.soq_assign.items():
if soq.binst.bloq_is(Join) and soq.idx == ():
dual_id = self._find_dual_on_line(rpos.y, rpos.seq_x, Split)
if dual_id is not None:
join_to_split[soq.binst.i] = dual_id # type: ignore[union-attr]
return join_to_split
from qualtran import BloqInstance
join_to_split = {}
for soq, rpos in self.soq_assign.items():
if isinstance(soq.binst, BloqInstance) and soq.binst.bloq_is(Join) and soq.idx == ():
dual_id = self._find_dual_on_line(rpos.y, rpos.seq_x, Split)
if dual_id is not None:
join_to_split[soq.binst.i] = dual_id
return join_to_split


def _build_split_to_join_map(self):
split_to_join = {}
for soq, rpos in self.soq_assign.items():
if soq.binst.bloq_is(Split) and soq.idx != ():
dual_id = self._find_dual_on_line(rpos.y, rpos.seq_x, Join)
if dual_id is not None:
split_to_join[soq.binst.i] = dual_id # type: ignore[union-attr]
return split_to_join
Comment on lines +55 to +61

Choose a reason for hiding this comment

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

critical

The soq.binst attribute can be a DanglingT instance, which does not have bloq_is or i attributes. Accessing them will lead to a runtime AttributeError. You should add a check to ensure soq.binst is a BloqInstance before accessing these attributes.

Suggested change
split_to_join = {}
for soq, rpos in self.soq_assign.items():
if soq.binst.bloq_is(Split) and soq.idx != ():
dual_id = self._find_dual_on_line(rpos.y, rpos.seq_x, Join)
if dual_id is not None:
split_to_join[soq.binst.i] = dual_id # type: ignore[union-attr]
return split_to_join
from qualtran import BloqInstance
split_to_join = {}
for soq, rpos in self.soq_assign.items():
if isinstance(soq.binst, BloqInstance) and soq.binst.bloq_is(Split) and soq.idx != ():
dual_id = self._find_dual_on_line(rpos.y, rpos.seq_x, Join)
if dual_id is not None:
split_to_join[soq.binst.i] = dual_id
return split_to_join


def maybe_reserve(self, binst, reg, idx):
# Reserve one slot so a partitioned wire can reclaim the same vertical region
# at its dual Join/Split.
if binst.bloq_is(Join) and reg.shape:
dual_id = self._join_to_split_id.get(binst.i)
self.reserve_n(1, lambda binst_to_check, reg_to_check: binst_to_check.i == dual_id)

if binst.bloq_is(Split) and not reg.shape:
dual_id = self._split_to_join_id.get(binst.i)
self.reserve_n(1, lambda binst_to_check, reg_to_check: binst_to_check.i == dual_id)
Comment on lines +63 to +72

Choose a reason for hiding this comment

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

critical

The binst argument can be a DanglingT instance, which does not have a bloq_is method. Calling it will lead to a runtime AttributeError. You should add a check to ensure binst is a BloqInstance before calling this method.

Suggested change
def maybe_reserve(self, binst, reg, idx):
# Reserve one slot so a partitioned wire can reclaim the same vertical region
# at its dual Join/Split.
if binst.bloq_is(Join) and reg.shape:
dual_id = self._join_to_split_id.get(binst.i)
self.reserve_n(1, lambda binst_to_check, reg_to_check: binst_to_check.i == dual_id)
if binst.bloq_is(Split) and not reg.shape:
dual_id = self._split_to_join_id.get(binst.i)
self.reserve_n(1, lambda binst_to_check, reg_to_check: binst_to_check.i == dual_id)
def maybe_reserve(self, binst, reg, idx):
from qualtran import BloqInstance
# Reserve one slot so a partitioned wire can reclaim the same vertical region
# at its dual Join/Split.
if isinstance(binst, BloqInstance) and binst.bloq_is(Join) and reg.shape:
dual_id = self._join_to_split_id.get(binst.i)
self.reserve_n(1, lambda binst_to_check, reg_to_check: binst_to_check.i == dual_id)
if isinstance(binst, BloqInstance) and binst.bloq_is(Split) and not reg.shape:
dual_id = self._split_to_join_id.get(binst.i)
self.reserve_n(1, lambda binst_to_check, reg_to_check: binst_to_check.i == dual_id)



handled_operations = {
ModPlus(): '"X"',
Circle(filled=True): '"•"',
Circle(filled=False): '"◦"',
LarrowTextBox(text='∧'): '"X"',
RarrowTextBox(text='∧'): '"X"',
}


def composite_bloq_to_quirk(
cbloq: CompositeBloq, line_manager: Optional[LineManager] = None, open_quirk: bool = False
) -> str:
"""Convert a CompositeBloq into a Quirk circuit URL."""
if line_manager is None:
line_manager = SparseLineManager(cbloq)

msd = get_musical_score_data(cbloq, manager=line_manager)

sparse_circuit = [(['1'] * (msd.max_y + 1)).copy() for _ in range(msd.max_x)]
for soq in msd.soqs:
try:
gate = handled_operations[soq.symb]
sparse_circuit[soq.rpos.seq_x][soq.rpos.y] = gate
except KeyError:
pass

empty_col = ['1'] * (msd.max_y + 1)
circuit = [col for col in sparse_circuit if col != empty_col]
if circuit == []:
raise ValueError(f"{cbloq} is an empty circuit")
nb_deleted_lines = 0
for i in range(
msd.max_y + 1
): # deleting lines of the circuit which are not used (happens with partition)
ind = i - nb_deleted_lines
for col in circuit:
line_is_useless = col[ind] == '1'
if not line_is_useless:
break
if line_is_useless:
for col in circuit:
col.pop(ind)
nb_deleted_lines += 1
Comment on lines +105 to +117

Choose a reason for hiding this comment

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

high

The current implementation for removing unused lines is hard to read and inefficient due to modifying lists while iterating over them. A more Pythonic and efficient approach would be to first identify the lines to keep and then construct a new circuit. This improves both readability and performance.

Suggested change
nb_deleted_lines = 0
for i in range(
msd.max_y + 1
): # deleting lines of the circuit which are not used (happens with partition)
ind = i - nb_deleted_lines
for col in circuit:
line_is_useless = col[ind] == '1'
if not line_is_useless:
break
if line_is_useless:
for col in circuit:
col.pop(ind)
nb_deleted_lines += 1
# deleting lines of the circuit which are not used (happens with partition)
if circuit:
num_lines = len(circuit[0])
lines_to_keep = [i for i in range(num_lines) if any(col[i] != '1' for col in circuit)]
circuit = [[col[i] for i in lines_to_keep] for col in circuit]


quirk_url = "https://algassert.com/quirk"
start = '#circuit={"cols":['
end = ']}'
url = quirk_url + start + ','.join('[' + ','.join(col) + ']' for col in circuit) + end

if open_quirk:
subprocess.run(["firefox", url], check=False)
Comment on lines +124 to +125

Choose a reason for hiding this comment

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

high

Using subprocess.run with a hardcoded "firefox" is not portable. The webbrowser module is the standard Python way to open a URL in the user's default browser, which is more portable and robust.

Suggested change
if open_quirk:
subprocess.run(["firefox", url], check=False)
if open_quirk:
import webbrowser
webbrowser.open(url)


return url


def bloq_to_quirk(
bloq: Bloq, line_manager: Optional[LineManager] = None, open_quirk: bool = False
) -> str:
"""Convert a Bloq into a Quirk circuit URL.

The input bloq is decomposed and flattened before conversion. Only a limited set
of operations is currently supported: control, anti-control, and NOT.

Args:
bloq: The bloq to export to Quirk.
line_manager: Line manager used to assign and order circuit lines.
open_quirk: If True, opens the generated URL in Firefox.

Returns:
A URL encoding the corresponding Quirk circuit.
"""
try:
cbloq = bloq.decompose_bloq().flatten()
except DecomposeTypeError: # no need to flatten the bloq if it is atomic
cbloq = bloq.as_composite_bloq()

return composite_bloq_to_quirk(cbloq, line_manager=line_manager, open_quirk=open_quirk)
64 changes: 64 additions & 0 deletions qualtran/quirk_interop/bloq_to_quirk_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import pytest

from qualtran import BloqBuilder, QAny, QUInt
from qualtran.bloqs.bookkeeping import Allocate, Join, Split
from qualtran.bloqs.basic_gates import Toffoli
from qualtran.bloqs.mcmt import MultiTargetCNOT
from qualtran.bloqs.arithmetic import Add, Negate
from qualtran.quirk_interop.bloq_to_quirk import (
SparseLineManager,
bloq_to_quirk,
composite_bloq_to_quirk,
)


def _build_split_join_split_cbloq(n):
bb = BloqBuilder()
q = bb.add(Allocate(QAny(n)))
qs = bb.add(Split(QAny(n)), reg=q)
q_joined = bb.add(Join(QAny(n)), reg=qs)
qs_again = bb.add(Split(QAny(n)), reg=q_joined)
out = bb.add(Join(QAny(n)), reg=qs_again)
return bb.finalize(out=out)


@pytest.mark.parametrize("n", range(3, 6))
def test_sparse_line_manager_builds_dual_maps(n):
cbloq = _build_split_join_split_cbloq(n)
manager = SparseLineManager(cbloq)

assert manager._join_to_split_id
assert manager._split_to_join_id


@pytest.mark.parametrize("n", range(3, 6))
def test_composite_bloq_to_quirk_url_shape(n):
cbloq = MultiTargetCNOT(n).decompose_bloq().flatten()
url = composite_bloq_to_quirk(cbloq)

assert url.startswith('https://algassert.com/quirk#circuit={"cols":[')
assert url.endswith(']}')


def test_bloq_to_quirk():
url_add = bloq_to_quirk(Add(QUInt(5)))
assert url_add.startswith('https://algassert.com/quirk#circuit={"cols":[')
assert url_add.endswith(']}')
url_mtcnot = bloq_to_quirk(MultiTargetCNOT(3))
assert (
url_mtcnot
== 'https://algassert.com/quirk#circuit={"cols":[[1,"•",1,"X"],[1,"•","X",1],["•","X",1,1],[1,"•","X",1],[1,"•",1,"X"]]}'
)


def test_negate_to_quirk():
url = bloq_to_quirk(Negate(QUInt(2)))
assert (
url
== 'https://algassert.com/quirk#circuit={"cols":[["X",1,1,1,1],[1,"X",1,1,1],[1,1,1,"X",1],[1,"•",1,"•","X"],["X",1,1,1,"•"],[1,"•",1,"•","X"],["X",1,"•",1,1],[1,"X",1,"•",1],[1,1,1,"X",1]]}'
)
Comment on lines +34 to +59
Copy link
Collaborator

Choose a reason for hiding this comment

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

these tests would break if we change the implementation details of MultiTargetCNOT, Add, Negate, ...

Can you just wire up a simple circuit with bloq builder for testing? then the actual circuit can be small too and the url won't be huge



def test_bloq_to_quirk_on_atomic():
url = bloq_to_quirk(Toffoli())
assert url == 'https://algassert.com/quirk#circuit={"cols":[["•","•","X"]]}'
Loading