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)
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)
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 ofQBits.
Registers#
reg: The register to be split. On its left, it is of the given data type. On the right, it is an array ofQBit()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)
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')
Partition#
Partition a generic index into multiple registers.
Parameters#
n: The total bitsize of the un-partitioned registerregs: Registers to partition into. Thesideattribute is ignored.partition:Falsemeans un-partition instead.
Registers#
x: the un-partitioned register. LEFT by default.[user spec]: The registers provided by theregsargument. 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)
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))
# 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())
Cast#
Cast a register from one n-bit QCDType to another QCDType.
This simply re-interprets the register’s data, and is a bookkeeping operation.
Parameters#
inp_dtype: Input QCDType to cast from.out_dtype: Output QCDType to cast to.shape: Optional multidimensional shape of the register to cast.allow_quantum_to_classical: Whether to allow (potentially dangerous) casting a quantum value to a classical value and vice versa. If you cast a classical bit to a qubit that was originally obtained by casting a qubit to a classical bit, the program will model unphysical quantum coherences that can give you fundamentally incorrect resource estimates. Use a measurement operation to convert a qubit to a classical bit (and correctly model the decoherence that results).
Registers#
reg: The quantum variable to cast
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')
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 thepartitionsargument below.partitions: A sequence of pairs specifying each register that is exposed in the external signature of theAutoPartitionand its relationship to the registers ofbloq. The first element of each pair is aRegisterexposed externally. The second is a list of register names ofbloqthat concatenate to form the externally exposed register. Ifbloqdoes not operate on some portion (ofnbits) of the externally exposed register, the sentinel valueUnused(n)can be used in place of a register name.left_only: If False, the output registers will also followpartition. Otherwise, the output registers will followbloq.signature.rights(). This flag must be set to True ifbloqdoes 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)
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: >)
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: >)
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: >)
Always#
Always execute the wrapped bloq, even when a controlled version is requested
A controlled version of a composite bloq in turn controls each subbloq in the decomposition.
Wrapping a particular subbloq with Always lets it bypass the controls,
i.e. it is “always” executed, irrespective of what the controls are.
This is useful when writing decompositions for two known patterns:
Compute-uncompute pairs: If a decomposition contains a compute-uncompute pair, then for a controlled version, we only need to control the rest of the bloqs. Wrapping both the compute and uncompute bloqs in
Alwayslets them bypass the controls.Controlled data-loading: For example, in the
AddKbloq which adds a constantkto the register, we (controlled) load the valuekinto a quantum register, and “always” perform an quantum-quantum addition usingAdd, and unloadk. Here wrapping the middleAddwithAlwayslets it bypass controls, e.g. when usingAddK.controlled().
This simplifies the decompositions by avoiding the need to explicitly define the decomposition for the controlled version of bloq.
Caution: This wrapper should be used with care. It is up to the bloq author to ensure that
the controlled version of a decomposition containing Always bloqs still respects the
controlled protocol. That is, ignoring controls on these subbloqs wrapped in Always should not
change the action of the overall bloq with respect to the reference controlled implementation.
Parameters#
subbloq: The bloq to always apply, irrespective of any controls.
from qualtran.bloqs.bookkeeping import Always
Example Instances#
from qualtran.bloqs.mcmt.and_bloq import And
always_and = Always(And())
Graphical Signature#
from qualtran.drawing import show_bloqs
show_bloqs([always_and],
['`always_and`'])
Call Graph#
from qualtran.resource_counting.generalizers import ignore_split_join
always_and_g, always_and_sigma = always_and.call_graph(max_depth=1, generalizer=ignore_split_join)
show_call_graph(always_and_g)
show_counts_sigma(always_and_sigma)
Counts totals:
And: 1
Using Always for a compute-uncompute pair#
Say we want to implement a doubly-controlled-Hadamard gate using an And and a CH.
We can wrap the Ands in Always, so that they ignore any controls, and only the CH gets controlled.
@attrs.frozen
class BloqWithComputeUncomputePair(Bloq):
@property
def signature(self):
return Signature.build(a=1, b=1, q=1)
def build_composite_bloq(self, bb, a, b, q):
from qualtran.bloqs.basic_gates import CHadamard
(a, b), c = bb.add(Always(And()), ctrl=[a, b])
c, q = bb.add(CHadamard(), ctrl=c, target=q)
(a, b) = bb.add(Always(And().adjoint()), ctrl=[a, b], target=c)
return {'a': a, 'b': b, 'q': q}
show_bloq(BloqWithComputeUncomputePair().decompose_bloq())
show_bloq(BloqWithComputeUncomputePair().controlled().decompose_bloq())