Bookkeeping Bloqs#

Bloqs for virtual operations and register reshaping.

from qualtran import Bloq, CompositeBloq, BloqBuilder, Signature, Register
from qualtran import QBit, QInt, QUInt, QAny
from qualtran.drawing import show_bloq, show_call_graph, show_counts_sigma
from typing import *
import numpy as np
import sympy
import cirq

Allocate#

Allocate an n bit register.

Parameters#

  • dtype: the quantum data type of the allocated register.

  • dirty: If true, represents a borrowing operation where allocated qubits can be dirty.

Registers#

  • reg [right]: The allocated register.

from qualtran.bloqs.bookkeeping import Allocate

Example Instances#

n = sympy.Symbol('n')
alloc = Allocate(QUInt(n))

Graphical Signature#

from qualtran.drawing import show_bloqs
show_bloqs([alloc],
           ['`alloc`'])

Call Graph#

from qualtran.resource_counting.generalizers import ignore_split_join
alloc_g, alloc_sigma = alloc.call_graph(max_depth=1, generalizer=ignore_split_join)
show_call_graph(alloc_g)
show_counts_sigma(alloc_sigma)
../../_images/5dbefeb75436109c084aed5edb3f91019f7235338d658da5ffee3072eb4c568a.svg

Counts totals:

  • Allocate: 1

Free#

Free (i.e. de-allocate) a register.

The tensor decomposition assumes the register is uncomputed and is in the zero state before getting freed. To verify that is the case, one can compute the resulting state vector after freeing qubits and make sure it is normalized.

Parameters#

  • dtype: The quantum data type of the register to be freed.

  • dirty: If true, represents adjoint of a borrowing operation where deallocated qubits were borrowed dirty from another part of the algorithm and must be free’d in the same dirty state.

Registers#

  • reg [left]: The register to free.

from qualtran.bloqs.bookkeeping import Free

Example Instances#

n = sympy.Symbol('n')
free = Free(QUInt(n))

Graphical Signature#

from qualtran.drawing import show_bloqs
show_bloqs([free],
           ['`free`'])

Call Graph#

from qualtran.resource_counting.generalizers import ignore_split_join
free_g, free_sigma = free.call_graph(max_depth=1, generalizer=ignore_split_join)
show_call_graph(free_g)
show_counts_sigma(free_sigma)
../../_images/5aba597c850d02b9bca772d10a71ff519e107ea29d99a4a719f0aabaa2d27088.svg

Counts totals:

  • Free: 1

Split#

Split a register of a given dtype into an array of QBits.

A logical operation may be defined on e.g. a quantum integer, but to define its decomposition we must operate on individual bits. Split can be used for this purpose. See Join for the inverse operation.

Parameters#

  • dtype: The quantum data type of the incoming data that will be split into an array of QBits.

Registers#

  • reg: The register to be split. On its left, it is of the given data type. On the right, it is an array of QBit()s of shape (dtype.num_qubits,).

from qualtran.bloqs.bookkeeping import Split

Example Instances#

split = Split(QUInt(4))

Graphical Signature#

from qualtran.drawing import show_bloqs
show_bloqs([split],
           ['`split`'])

Join#

Join an array of QBits into one register of type dtype.

Parameters#

  • dtype: The quantum data type of the right (joined) register.

Registers#

  • reg: The register to be joined. On its left, it is an array of qubits. On the right, it is a register of the given data type.

from qualtran.bloqs.bookkeeping import Join

Example Instances#

join = Join(dtype=QUInt(4))

Graphical Signature#

from qualtran.drawing import show_bloqs
show_bloqs([join],
           ['`join`'])

Combining Split and Join#

As a brief example, we compose split and join into an identity operation.

import attrs

@attrs.frozen
class SplitJoin(Bloq):
    n: int

    @property
    def signature(self) -> Signature:
        return Signature([Register('x', QAny(self.n))])

    def build_composite_bloq(
        self, bb: 'BloqBuilder', *, x: 'Soquet'
    ) -> Dict[str, 'Soquet']:
        xs = bb.split(x)
        x = bb.join(xs)
        return {'x': x}

split_join = SplitJoin(n=4).decompose_bloq()
show_bloq(split_join)
../../_images/8536af7b9b0a9b20104662f90446c8b3f18d8c0bcb21161fdc1747a8639cd0a8.svg

In the “musical score” diagrams, splits are drawn such that the dtype wire is terminated, and the array-of-bits wires are started; and vice-versa for join.

show_bloq(split_join, 'musical_score')
../../_images/f91c6d5799f5fba35da0c507a33a7cb2543f057f627a713f4231a529e3c0c952.png

Partition#

Partition a generic index into multiple registers.

Parameters#

  • n: The total bitsize of the un-partitioned register

  • regs: Registers to partition into. The side attribute is ignored.

  • partition: False means un-partition instead.

Registers#

  • x: the un-partitioned register. LEFT by default.

  • [user spec]: The registers provided by the regs argument. RIGHT by default.

from qualtran.bloqs.bookkeeping import Partition

Example Instances#

regs = (Register('xx', QAny(2), shape=(2, 3)), Register('yy', QAny(37)))
bitsize = sum(reg.total_bits() for reg in regs)
partition = Partition(n=bitsize, regs=regs)

Graphical Signature#

from qualtran.drawing import show_bloqs
show_bloqs([partition],
           ['`partition`'])

As an example of the utility of Partition, we’ll use the generic TestMultiRegister bloq as an example sub-bloq with many registers. We can wrap it in the BlackBoxBloq adapter defined below to abstract away the complicated signature into one register named “system”.

from qualtran.bloqs.for_testing.many_registers import TestMultiRegister

subbloq = TestMultiRegister()
show_bloq(subbloq)
../../_images/27ec0e7b103a136dd0fb53c6322b9443ef261e4167e67dbba4d2c91df7b5c8eb.svg
import attrs

@attrs.frozen
class BlackBoxBloq(Bloq):
    subbloq: Bloq

    @property
    def signature(self) -> Signature:
        return Signature.build(system=self.bitsize)

    @property
    def bitsize(self):
        return sum(reg.total_bits() for reg in self.subbloq.signature)

    def build_composite_bloq(self, bb: 'BloqBuilder', system: 'SoquetT') -> Dict[str, 'Soquet']:
        bloq_regs = self.subbloq.signature
        partition = Partition(self.bitsize, bloq_regs)
        partitioned_vars = bb.add(partition, x=system)
        partitioned_vars = bb.add(
            self.subbloq, **{reg.name: sp for reg, sp in zip(bloq_regs, partitioned_vars)}
        )
        system = bb.add(
            partition.adjoint(), **{reg.name: sp for reg, sp in zip(bloq_regs, partitioned_vars)}
        )
        return {'system': system}
# The signature is now just one register named "system"
show_bloq(BlackBoxBloq(subbloq))
../../_images/1db7526c8da9c309d24637416621eb32c584cb2f6458b5640f002f0d206ac809.svg
# The `Partition` bloq partitions the one "system" register into the quantum interface
# expected by the subbloq (and back again).
show_bloq(BlackBoxBloq(subbloq).decompose_bloq())
../../_images/d3862803c29d4820583afdf41223d91d94990e53ed26bc891a311ac3b1c4c009.svg

Cast#

Cast a register from one n-bit QDType to another QDType.

This re-interprets the register’s data type from inp_dtype to out_dtype.

Parameters#

  • inp_dtype: Input QDType to cast from.

  • out_dtype: Output QDType to cast to.

  • shape: shape of the register to cast.

Registers#

  • in: input register to cast from.

  • out: input register to cast to.

from qualtran.bloqs.bookkeeping import Cast

Example Instances#

from qualtran import QFxp, QInt

cast = Cast(QInt(32), QFxp(32, 32))

Graphical Signature#

from qualtran.drawing import show_bloqs
show_bloqs([cast],
           ['`cast`'])

Annotating diagrams with DTypes#

Here, we see an example where the Cast re-interprets the input QFxp register as a QUInt so an addition can be performed. We annotate the compute graph wires with their quantum data types by using type='dtype' in the call to show_bloq.

from qualtran.bloqs.for_testing import TestCastToFrom
show_bloq(TestCastToFrom().decompose_bloq(), type='dtype')
../../_images/2d95ac1c90add8a7aeaed64a47cc1542a373a528144737e31f1b84ca44c74147.svg

AutoPartition#

Automatically adds and undoes Partition of registers to match the signature of a sub-bloq.

This tool enables using a bloq in a context expecting an alternative signature that combines registers in the bloq’s signature or operates over more registers than the bloq does. For example, it can adapt a bloq exposing multiple selection registers to a quantum interface that expects only one unified selection register.

Wrapping in AutoPartition also hides splits and joins behind a level of decomposition, which can produce more helpful circuit diagrams compared to manually splitting and joining.

Parameters#

  • bloq: The sub-bloq to wrap. Its register names are used within the second items in each pair in the partitions argument below.

  • partitions: A sequence of pairs specifying each register that is exposed in the external signature of the AutoPartition and its relationship to the registers of bloq. The first element of each pair is a Register exposed externally. The second is a list of register names of bloq that concatenate to form the externally exposed register. If bloq does not operate on some portion (of n bits) of the externally exposed register, the sentinel value Unused(n) can be used in place of a register name.

  • left_only: If False, the output registers will also follow partition. Otherwise, the output registers will follow bloq.signature.rights(). This flag must be set to True if bloq does not have the same LEFT and RIGHT registers, as is required for the bloq to be fully wrapped on the left and right.

Registers#

  • [user_spec]: The output registers of the wrapped bloq.

from qualtran.bloqs.bookkeeping import AutoPartition

Example Instances#

from qualtran import Controlled, CtrlSpec
from qualtran.bloqs.basic_gates import Swap

bloq = Controlled(Swap(1), CtrlSpec())
auto_partition = AutoPartition(
    bloq, [(Register('x', QAny(2)), ['ctrl', 'x']), (Register('y', QAny(1)), ['y'])]
)
from qualtran import Controlled, CtrlSpec
from qualtran.bloqs.basic_gates import Swap
from qualtran.bloqs.bookkeeping.auto_partition import Unused

bloq = Controlled(Swap(1), CtrlSpec())
auto_partition_unused = AutoPartition(
    bloq,
    [
        (Register('x', QAny(3)), ['ctrl', 'x', Unused(1)]),
        (Register('y', QAny(1)), ['y']),
        (Register('z', QAny(2)), [Unused(2)]),
    ],
)

Graphical Signature#

from qualtran.drawing import show_bloqs
show_bloqs([auto_partition, auto_partition_unused],
           ['`auto_partition`', '`auto_partition_unused`'])

Call Graph#

from qualtran.resource_counting.generalizers import ignore_split_join
auto_partition_g, auto_partition_sigma = auto_partition.call_graph(max_depth=1, generalizer=ignore_split_join)
show_call_graph(auto_partition_g)
show_counts_sigma(auto_partition_sigma)
../../_images/2c5831b7378b0fe27302b7712b2155cb19befaa6f0dccca0bc75c850b2c70681.svg

Counts totals:

  • TwoBitCSwap: 1

Using AutoPartition to simplify diagrams#

Sometimes, we want to use bloqs whose signatures don’t quite match up with the signature of a bigger bloq we want to build. For example, the bloq might have a flat list of qubits whereas we have a big register, or vice versa.

Normally, we can just split / join / partition during the decomposition and go on our way, but this leads to unsightly diagrams like this:

from functools import cached_property

from qualtran.bloqs.rotations.hamming_weight_phasing import HammingWeightPhasing
from qualtran.drawing import draw_musical_score
from qualtran.drawing.musical_score import get_musical_score_data


@attrs.frozen
class ManyBit(Bloq):
    @cached_property
    def signature(self) -> Signature:
        return Signature((Register('xs', QBit(), shape=(20,)),))


@attrs.frozen
class NotWrapped(Bloq):
    bitsize: int = 10

    @cached_property
    def signature(self) -> Signature:
        return Signature((Register('x', QBit(), shape=(self.bitsize,)), Register('y', QAny(20))))

    def build_composite_bloq(
        self, bb: BloqBuilder, x: 'SoquetT', y: 'SoquetT'
    ) -> Dict[str, 'SoquetT']:
        for i in range(5):
            two_bit = bb.join(x[i * 2 : i * 2 + 2], QUInt(2))
            two_bit = bb.add(HammingWeightPhasing(2, 0.11), x=two_bit)
            x[i * 2 : i * 2 + 2] = bb.split(two_bit)
        many_bit = bb.split(y)
        many_bit = bb.add(ManyBit(), xs=many_bit)
        return {'x': x, 'y': bb.join(many_bit)}


bloq = NotWrapped()
draw_musical_score(get_musical_score_data(bloq.decompose_bloq()))
(<Figure size 533.333x800 with 1 Axes>, <Axes: >)
../../_images/6483e4ef80a352c59db1dcc7422bea857c487172e057fca76a2f980f164b2fbf.png

Using the AutoPartition bloq, we can hide the partition/unpartition pairs behind a level of decomposition, thereby cleaning up the diagram:

@attrs.frozen
class Wrapped(Bloq):
    bitsize: int = 10

    @cached_property
    def signature(self) -> Signature:
        return Signature((Register('x', QBit(), shape=(self.bitsize,)), Register('y', QAny(20))))

    def build_composite_bloq(
        self, bb: BloqBuilder, x: 'SoquetT', y: 'SoquetT'
    ) -> Dict[str, 'SoquetT']:
        for i in range(5):
            hwp = HammingWeightPhasing(2, i * 0.11)
            x[i * 2 : i * 2 + 2] = bb.add(
                AutoPartition(hwp, [(Register('reg_1', QBit(), shape=(2,)), ('x',))]),
                reg_1=x[i * 2 : i * 2 + 2],
            )
        many = ManyBit()
        b = AutoPartition(many, [(Register('y', QAny(20)), ('xs',))])
        y = bb.add(b, y=y)
        return {'x': x, 'y': y}


bloq = Wrapped()
draw_musical_score(get_musical_score_data(bloq.decompose_bloq()))
(<Figure size 626.087x800 with 1 Axes>, <Axes: >)
../../_images/1393f7566a8c600aa12366afca0fa38c1523105a4884c9091070d9bb39b83102.png

Instead of explicitly instantiating a AutoPartition, we can also use the utility function BloqBuilder.add_and_partition:

@attrs.frozen
class Wrapped(Bloq):
    bitsize: int = 10

    @cached_property
    def signature(self) -> Signature:
        return Signature((Register('x', QBit(), shape=(self.bitsize,)), Register('y', QAny(20))))

    def build_composite_bloq(
        self, bb: BloqBuilder, x: 'SoquetT', y: 'SoquetT'
    ) -> Dict[str, 'SoquetT']:
        for i in range(5):
            hwp = HammingWeightPhasing(2, i * 0.11)
            x[i * 2 : i * 2 + 2] = bb.add_and_partition(
                hwp, [(Register('reg_1', QBit(), shape=(2,)), ('x',))], reg_1=x[i * 2 : i * 2 + 2]
            )
        many = ManyBit()
        y = bb.add_and_partition(many, [(Register('y', QAny(20)), ('xs',))], y=y)
        return {'x': x, 'y': y}


bloq = Wrapped()
draw_musical_score(get_musical_score_data(bloq.decompose_bloq()))
(<Figure size 626.087x800 with 1 Axes>, <Axes: >)
../../_images/1393f7566a8c600aa12366afca0fa38c1523105a4884c9091070d9bb39b83102.png