From a6552cc8c3dd6d2b15cb45af048898aa1cd35891 Mon Sep 17 00:00:00 2001 From: Axel Pappalardo Date: Thu, 26 Feb 2026 10:39:34 +0100 Subject: [PATCH 1/4] feat: added function to give quirk url from a qualtran's bloq --- qualtran/quirk_interop/bloq_to_quirk.py | 106 +++++++++++++++++++ qualtran/quirk_interop/bloq_to_quirk_test.py | 14 +++ 2 files changed, 120 insertions(+) create mode 100644 qualtran/quirk_interop/bloq_to_quirk.py create mode 100644 qualtran/quirk_interop/bloq_to_quirk_test.py diff --git a/qualtran/quirk_interop/bloq_to_quirk.py b/qualtran/quirk_interop/bloq_to_quirk.py new file mode 100644 index 000000000..e583e8319 --- /dev/null +++ b/qualtran/quirk_interop/bloq_to_quirk.py @@ -0,0 +1,106 @@ +import subprocess + +from qualtran import Bloq, DecomposeTypeError +from qualtran.bloqs.bookkeeping import Join, Split +from qualtran.drawing import ( + ModPlus, + Circle, + LarrowTextBox, + RarrowTextBox, + LineManager, + get_musical_score_data, +) + + +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 maybe_reserve(self, binst, reg, idx): + if binst.bloq_is(Join) and reg.shape: + + def _keep_split_lines(binst_to_check, reg_to_check): + binst_cond = binst_to_check.bloq == binst.bloq.adjoint() + reg_cond = reg_to_check == reg.adjoint() + return binst_cond and reg_cond + + self.reserve_n(1, _keep_split_lines) + + if binst.bloq_is(Split) and not reg.shape: + + def _keep_joined_line(binst_to_check, reg_to_check): + binst_cond = binst_to_check.bloq == binst.bloq.adjoint() + reg_cond = reg_to_check == reg.adjoint() + return binst_cond and reg_cond + + self.reserve_n(1, _keep_joined_line) + + +handled_operations = { + ModPlus(): '"X"', + Circle(filled=True): '"•"', + Circle(filled=False): '"◦"', + LarrowTextBox(text='∧'): '"X"', + RarrowTextBox(text='∧'): '"X"', +} + + +def bloq_to_quirk( + bloq: Bloq, + line_manager: LineManager = SparseLineManager().__init__(), # type: ignore[misc] + open_quirk=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: + flat_bloq = bloq.decompose_bloq().flatten() + except DecomposeTypeError: # no need to flatten the bloq if it is atomic + flat_bloq = bloq.as_composite_bloq() + msd = get_musical_score_data(flat_bloq, manager=line_manager) + + sparse_circuit = [(['1'] * (msd.max_y + 1)).copy() for _ in range(msd.max_x)] + for soq in msd.json_dict()['soqs']: + try: + gate = handled_operations[soq.symb] + sparse_circuit[soq.rpos.seq_x][soq.rpos.y] = gate + except KeyError: + None + + circuit = list(filter((['1'] * (msd.max_y + 1)).__ne__, sparse_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 + + 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) + + return url diff --git a/qualtran/quirk_interop/bloq_to_quirk_test.py b/qualtran/quirk_interop/bloq_to_quirk_test.py new file mode 100644 index 000000000..fff87111d --- /dev/null +++ b/qualtran/quirk_interop/bloq_to_quirk_test.py @@ -0,0 +1,14 @@ +from qualtran import QUInt +from qualtran.bloqs.basic_gates import Toffoli +from qualtran.bloqs.mcmt import MultiTargetCNOT +from qualtran.bloqs.arithmetic.addition import Add +from qualtran.quirk_interop.bloq_to_quirk import bloq_to_quirk + + +def test_bloq_to_quirk(): + bloq_to_quirk(Add(QUInt(5))) + bloq_to_quirk(MultiTargetCNOT(5)) + + +def test_bloq_to_quirk_on_atomic(): + bloq_to_quirk(Toffoli()) From 9d33db3928e6b56aaa80b5e0d381786cb397ee1c Mon Sep 17 00:00:00 2001 From: Axel Pappalardo Date: Thu, 26 Feb 2026 13:57:12 +0100 Subject: [PATCH 2/4] fix: linting fix --- qualtran/quirk_interop/bloq_to_quirk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qualtran/quirk_interop/bloq_to_quirk.py b/qualtran/quirk_interop/bloq_to_quirk.py index e583e8319..eaf39b930 100644 --- a/qualtran/quirk_interop/bloq_to_quirk.py +++ b/qualtran/quirk_interop/bloq_to_quirk.py @@ -17,7 +17,8 @@ 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)) + # DIDN'TDO: only handles partition patterns of the type (QAny(n)/QUInt(n)/... -> QBit((n,)) + # or QBit((n,)) -> QAny(n)) def maybe_reserve(self, binst, reg, idx): if binst.bloq_is(Join) and reg.shape: From 372679107ca2b6b722136239c0daa22e82bf0782 Mon Sep 17 00:00:00 2001 From: Axel Pappalardo Date: Wed, 4 Mar 2026 14:36:29 +0100 Subject: [PATCH 3/4] fix: implemented recommended correction + bug correction of SparseLineManager + more testing --- qualtran/quirk_interop/bloq_to_quirk.py | 124 +++++++++++++------ qualtran/quirk_interop/bloq_to_quirk_test.py | 62 +++++++++- 2 files changed, 140 insertions(+), 46 deletions(-) diff --git a/qualtran/quirk_interop/bloq_to_quirk.py b/qualtran/quirk_interop/bloq_to_quirk.py index eaf39b930..5dbcefa81 100644 --- a/qualtran/quirk_interop/bloq_to_quirk.py +++ b/qualtran/quirk_interop/bloq_to_quirk.py @@ -1,6 +1,7 @@ import subprocess +from typing import Optional -from qualtran import Bloq, DecomposeTypeError +from qualtran import Bloq, DecomposeTypeError, CompositeBloq from qualtran.bloqs.bookkeeping import Join, Split from qualtran.drawing import ( ModPlus, @@ -10,34 +11,65 @@ 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) + ] + 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 + + 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 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: - - def _keep_split_lines(binst_to_check, reg_to_check): - binst_cond = binst_to_check.bloq == binst.bloq.adjoint() - reg_cond = reg_to_check == reg.adjoint() - return binst_cond and reg_cond - - self.reserve_n(1, _keep_split_lines) + 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: - - def _keep_joined_line(binst_to_check, reg_to_check): - binst_cond = binst_to_check.bloq == binst.bloq.adjoint() - reg_cond = reg_to_check == reg.adjoint() - return binst_cond and reg_cond - - self.reserve_n(1, _keep_joined_line) + 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 = { @@ -49,39 +81,27 @@ def _keep_joined_line(binst_to_check, reg_to_check): } -def bloq_to_quirk( - bloq: Bloq, - line_manager: LineManager = SparseLineManager().__init__(), # type: ignore[misc] - open_quirk=False, +def composite_bloq_to_quirk( + cbloq: CompositeBloq, line_manager: Optional[LineManager] = None, open_quirk: bool = False ) -> str: - """Convert a Bloq into a Quirk circuit URL. + """Convert a CompositeBloq into a Quirk circuit URL.""" + if line_manager is None: + line_manager = SparseLineManager(cbloq) - 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: - flat_bloq = bloq.decompose_bloq().flatten() - except DecomposeTypeError: # no need to flatten the bloq if it is atomic - flat_bloq = bloq.as_composite_bloq() - msd = get_musical_score_data(flat_bloq, manager=line_manager) + 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.json_dict()['soqs']: + for soq in msd.soqs: try: gate = handled_operations[soq.symb] sparse_circuit[soq.rpos.seq_x][soq.rpos.y] = gate except KeyError: - None + pass - circuit = list(filter((['1'] * (msd.max_y + 1)).__ne__, sparse_circuit)) + 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 @@ -105,3 +125,27 @@ def bloq_to_quirk( subprocess.run(["firefox", url], check=False) 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) diff --git a/qualtran/quirk_interop/bloq_to_quirk_test.py b/qualtran/quirk_interop/bloq_to_quirk_test.py index fff87111d..367824dc3 100644 --- a/qualtran/quirk_interop/bloq_to_quirk_test.py +++ b/qualtran/quirk_interop/bloq_to_quirk_test.py @@ -1,14 +1,64 @@ -from qualtran import QUInt +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.addition import Add -from qualtran.quirk_interop.bloq_to_quirk import bloq_to_quirk +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(): - bloq_to_quirk(Add(QUInt(5))) - bloq_to_quirk(MultiTargetCNOT(5)) + 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]]}' + ) def test_bloq_to_quirk_on_atomic(): - bloq_to_quirk(Toffoli()) + url = bloq_to_quirk(Toffoli()) + assert url == 'https://algassert.com/quirk#circuit={"cols":[["•","•","X"]]}' From 08e21cdf62994538fda7343436a3d153490fd6e7 Mon Sep 17 00:00:00 2001 From: Axel Pappalardo Date: Fri, 13 Mar 2026 17:41:48 +0100 Subject: [PATCH 4/4] fix: implemented change recommended in review + added custom bloq for testing --- qualtran/quirk_interop/bloq_to_quirk.py | 20 +++----- qualtran/quirk_interop/bloq_to_quirk_test.py | 52 ++++++++++---------- 2 files changed, 32 insertions(+), 40 deletions(-) diff --git a/qualtran/quirk_interop/bloq_to_quirk.py b/qualtran/quirk_interop/bloq_to_quirk.py index 5dbcefa81..7cb469b88 100644 --- a/qualtran/quirk_interop/bloq_to_quirk.py +++ b/qualtran/quirk_interop/bloq_to_quirk.py @@ -31,7 +31,7 @@ def __init__(self, cbloq: CompositeBloq, max_n_lines: int = 100): 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): + def _find_dual_on_line(self, line: int, start: int, dual_cls: Bloq): dual_candidates = [ (rpos.seq_x, soq.binst.i) # type: ignore[union-attr] for soq, rpos in self.soq_assign.items() @@ -102,19 +102,11 @@ def composite_bloq_to_quirk( 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 + # 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":[' diff --git a/qualtran/quirk_interop/bloq_to_quirk_test.py b/qualtran/quirk_interop/bloq_to_quirk_test.py index 367824dc3..9f6dde4d8 100644 --- a/qualtran/quirk_interop/bloq_to_quirk_test.py +++ b/qualtran/quirk_interop/bloq_to_quirk_test.py @@ -1,10 +1,8 @@ import pytest -from qualtran import BloqBuilder, QAny, QUInt +from qualtran import BloqBuilder, QAny +from qualtran.bloqs.basic_gates import Toffoli, XGate 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, @@ -14,12 +12,21 @@ 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) + q1 = bb.add(Allocate(QAny(n))) + q2 = bb.add(Allocate(QAny(n))) + qs1 = bb.add(Split(QAny(n)), reg=q1) + qs2 = bb.add(Split(QAny(n)), reg=q2) + for i in range(n): + qs1[i] = bb.add(XGate(), q=qs1[i]) + q1 = bb.add(Join(QAny(n)), reg=qs1) + q2 = bb.add(Join(QAny(n)), reg=qs2) + qs1 = bb.add(Split(QAny(n)), reg=q1) + qs2 = bb.add(Split(QAny(n)), reg=q2) + for i in range(n): + qs2[i] = bb.add(XGate(), q=qs2[i]) + q2 = bb.add(Join(QAny(n)), reg=qs2) + q1 = bb.add(Join(QAny(n)), reg=qs1) + return bb.finalize(q1=q1, q2=q2) @pytest.mark.parametrize("n", range(3, 6)) @@ -33,29 +40,22 @@ def test_sparse_line_manager_builds_dual_maps(n): @pytest.mark.parametrize("n", range(3, 6)) def test_composite_bloq_to_quirk_url_shape(n): - cbloq = MultiTargetCNOT(n).decompose_bloq().flatten() + cbloq = _build_split_join_split_cbloq(n) 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))) +def test_composite_bloq_to_quirk(): + cbloq1 = _build_split_join_split_cbloq(1) + url1 = composite_bloq_to_quirk(cbloq1) + assert url1 == 'https://algassert.com/quirk#circuit={"cols":[["X",1],[1,"X"]]}' + cbloq2 = _build_split_join_split_cbloq(2) + url2 = composite_bloq_to_quirk(cbloq2) 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]]}' + url2 + == 'https://algassert.com/quirk#circuit={"cols":[["X",1,1,1],[1,"X",1,1],[1,1,"X",1],[1,1,1,"X"]]}' )