Block system
qadence offers a block-based system to construct quantum circuits in a flexible manner.
AbstractBlock(tag=None, __array_priority__=1000)
dataclass
Section titled “
AbstractBlock(tag=None, __array_priority__=1000)
dataclass
”
Bases: ABC
Base class for both primitive and composite blocks.
| ATTRIBUTE | DESCRIPTION |
|---|---|
name |
A human-readable name attached to the block type. Notice, this is the same for all the class instances so it cannot be used for identifying different blocks
TYPE:
|
qubit_support |
The qubit support of the block expressed as a tuple of integers
TYPE:
|
tag |
A tag identifying a particular instance of the block which can be used for identification and pretty printing
TYPE:
|
eigenvalues |
The eigenvalues of the matrix representing the block. This is used mainly for primitive blocks and it's needed for generalized parameter shift rule computations. Currently unused.
TYPE:
|
is_identity
property
Section titled “
is_identity
property
”Identity predicate for blocks.
n_qubits()
Section titled “
n_qubits()
”The number of qubits in the whole system.
A block acting on qubit N would has at least n_qubits >= N + 1.
Source code in qadence/blocks/abstract.py
47484950515253@abstractpropertydef n_qubits(self) -> int: """The number of qubits in the whole system.
A block acting on qubit N would has at least n_qubits >= N + 1. """ pass
n_supports()
Section titled “
n_supports()
”The number of qubits the block is acting on.
Source code in qadence/blocks/abstract.py
55565758@abstractpropertydef n_supports(self) -> int: """The number of qubits the block is acting on.""" pass
qubit_support()
Section titled “
qubit_support()
”The indices of the qubit(s) the block is acting on.
Qadence uses the ordering [0..,N-1] for qubits.
Source code in qadence/blocks/abstract.py
39404142434445@abstractpropertydef qubit_support(self) -> Tuple[int, ...]: """The indices of the qubit(s) the block is acting on.
Qadence uses the ordering [0..,N-1] for qubits. """ passPrimitive blocks
Section titled “Primitive blocks”
ControlBlock(control, target_block, noise=None)
Section titled “
ControlBlock(control, target_block, noise=None)
”
Bases: PrimitiveBlock
The abstract ControlBlock.
Source code in qadence/blocks/primitive.py
372373374375376377378379380381382383384def __init__( self, control: tuple[int, ...], target_block: PrimitiveBlock, noise: NoiseHandler | None = None,) -> None: self.control = control self.blocks = (target_block,) self.target = target_block.qubit_support
# using tuple expansion because some control operations could # have multiple targets, e.g. CSWAP super().__init__((*control, *self.target), noise=noise) # target_block.qubit_support[0]))
ParametricBlock(qubit_support, noise=None)
dataclass
Section titled “
ParametricBlock(qubit_support, noise=None)
dataclass
”
Bases: PrimitiveBlock
Parameterized primitive blocks.
Source code in qadence/blocks/primitive.py
37383940414243def __init__( self, qubit_support: tuple[int, ...], noise: NoiseHandler | None = None,): self._qubit_support = qubit_support self._noise = noise
num_parameters()
abstractmethod
Section titled “
num_parameters()
abstractmethod
”The number of parameters required by the block.
This is a class property since the number of parameters is defined automatically before instantiating the operation. Also, this could correspond to a larger number of actual user-facing parameters since any parameter expression is allowed
Examples: - RX operation has 1 parameter - U operation has 3 parameters - HamEvo has 2 parameters (generator and time evolution)
Source code in qadence/blocks/primitive.py
157158159160161162163164165166167168169170171@abstractmethoddef num_parameters(cls) -> int: """The number of parameters required by the block.
This is a class property since the number of parameters is defined automatically before instantiating the operation. Also, this could correspond to a larger number of actual user-facing parameters since any parameter expression is allowed
Examples: - RX operation has 1 parameter - U operation has 3 parameters - HamEvo has 2 parameters (generator and time evolution) """ pass
ParametricControlBlock(control, target_block, noise=None)
Section titled “
ParametricControlBlock(control, target_block, noise=None)
”
Bases: ParametricBlock
The abstract parametrized ControlBlock.
Source code in qadence/blocks/primitive.py
437438439440441442443444445446def __init__( self, control: tuple[int, ...], target_block: ParametricBlock, noise: NoiseHandler | None = None,) -> None: self.blocks = (target_block,) self.control = control self.parameters = target_block.parameters super().__init__((*control, *target_block.qubit_support), noise=noise)
PrimitiveBlock(qubit_support, noise=None)
Section titled “
PrimitiveBlock(qubit_support, noise=None)
”
Bases: AbstractBlock
Primitive blocks represent elementary unitary operations.
Examples are single/multi-qubit gates or Hamiltonian evolution.
See qadence.operations for a full list of
primitive blocks.
Source code in qadence/blocks/primitive.py
37383940414243def __init__( self, qubit_support: tuple[int, ...], noise: NoiseHandler | None = None,): self._qubit_support = qubit_support self._noise = noise
digital_decomposition()
Section titled “
digital_decomposition()
”Decomposition into purely digital gates.
This method returns a decomposition of the Block in a combination of purely digital single-qubit and two-qubit 'gates', by manual/custom knowledge of how this can be done efficiently. :return:
Source code in qadence/blocks/primitive.py
535455565758596061def digital_decomposition(self) -> AbstractBlock: """Decomposition into purely digital gates.
This method returns a decomposition of the Block in a combination of purely digital single-qubit and two-qubit 'gates', by manual/custom knowledge of how this can be done efficiently. :return: """ return self
ProjectorBlock(ket, bra, qubit_support, noise=None)
Section titled “
ProjectorBlock(ket, bra, qubit_support, noise=None)
”
Bases: PrimitiveBlock
The abstract ProjectorBlock.
Arguments:
ket (str): The ket given as a bitstring.bra (str): The bra given as a bitstring.qubit_support (int | tuple[int]): The qubit_support of the block.Source code in qadence/blocks/primitive.py
519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550def __init__( self, ket: str, bra: str, qubit_support: int | tuple[int, ...], noise: NoiseHandler | None = None,) -> None: """ Arguments:
ket (str): The ket given as a bitstring. bra (str): The bra given as a bitstring. qubit_support (int | tuple[int]): The qubit_support of the block. """ if isinstance(qubit_support, int): qubit_support = (qubit_support,) if len(bra) != len(ket): raise ValueError( "Bra and ket must be bitstrings of same length in the 'Projector' definition." ) elif len(bra) != len(qubit_support): raise ValueError("Bra or ket must be of same length as the 'qubit_support'") for wf in [bra, ket]: if not all(int(item) == 0 or int(item) == 1 for item in wf): raise ValueError( "All qubits must be either in the '0' or '1' state" " in the 'ProjectorBlock' definition." )
self.ket = ket self.bra = bra super().__init__(qubit_support, noise=noise)
ScaleBlock(block, parameter)
Section titled “
ScaleBlock(block, parameter)
”
Bases: ParametricBlock
Scale blocks are created when multiplying a block by a number or parameter.
Example:
from qadence import X
print(X(0) * 2)[mul: 2]└── X(0)Source code in qadence/blocks/primitive.py
239240241242243244245def __init__(self, block: AbstractBlock, parameter: Any): self.block = block # TODO: more meaningful name like `scale`? self.parameters = ( parameter if isinstance(parameter, ParamMap) else ParamMap(parameter=parameter) ) super().__init__(block.qubit_support)
TimeEvolutionBlock(qubit_support, noise=None)
dataclass
Section titled “
TimeEvolutionBlock(qubit_support, noise=None)
dataclass
”
Bases: ParametricBlock
Simple time evolution block with time-independent Hamiltonian.
This class is just a convenience class which is used to label blocks which contains simple time evolution with time-independent Hamiltonian operators
Source code in qadence/blocks/primitive.py
37383940414243def __init__( self, qubit_support: tuple[int, ...], noise: NoiseHandler | None = None,): self._qubit_support = qubit_support self._noise = noiseAnalog blocks
Section titled “Analog blocks”To learn how to use analog blocks and how to mix digital & analog blocks, check out the digital-analog section of the documentation.
Examples on how to use digital-analog blocks can be found in the *examples folder of the qadence repo:
- Fit a simple sinus:
examples/digital-analog/fit-sin.py - Solve a QUBO:
examples/digital-analog/qubo.py
AnalogChain(blocks)
dataclass
Section titled “
AnalogChain(blocks)
dataclass
”
Bases: AnalogComposite
A chain of analog blocks.
Needed because analog blocks require
stricter validation than the general ChainBlock.
AnalogChains can only be constructed from AnalogKron blocks or
globally supported, primitive, analog blocks (like InteractionBlocks and
ConstantAnalogRotations).
Automatically constructed by the chain
function if only analog blocks are given.
Example:
from qadence import X, chain, AnalogInteraction
b = chain(AnalogInteraction(200), AnalogInteraction(200))print(type(b)) # this is an `AnalogChain`
b = chain(X(0), AnalogInteraction(200))print(type(b)) # this is a general `ChainBlock`<class 'qadence.blocks.analog.AnalogChain'><class 'qadence.blocks.composite.ChainBlock'>Source code in qadence/blocks/analog.py
250251252253254255256257258259260261262263264265266267268269270271272273274275276277def __init__(self, blocks: Tuple[AnalogBlock, ...]): """A chain of analog blocks.
Needed because analog blocks require stricter validation than the general `ChainBlock`.
`AnalogChain`s can only be constructed from `AnalogKron` blocks or _**globally supported**_, primitive, analog blocks (like `InteractionBlock`s and `ConstantAnalogRotation`s).
Automatically constructed by the [`chain`][qadence.blocks.utils.chain] function if only analog blocks are given.
Example: ```python exec="on" source="material-block" result="json" from qadence import X, chain, AnalogInteraction
b = chain(AnalogInteraction(200), AnalogInteraction(200)) print(type(b)) # this is an `AnalogChain`
b = chain(X(0), AnalogInteraction(200)) print(type(b)) # this is a general `ChainBlock` ``` """ for b in blocks: if not (isinstance(b, AnalogKron) or b.qubit_support.is_global): raise ValueError("Only KronBlocks or global blocks can be chain'ed.") self.blocks = blocks
AnalogKron(blocks, interaction=Interaction.NN)
dataclass
Section titled “
AnalogKron(blocks, interaction=Interaction.NN)
dataclass
”
Bases: AnalogComposite
Stack analog blocks vertically (i.e. in time).
Needed because analog require
stricter validation than the general KronBlock.
AnalogKrons can only be constructed from non-global, analog blocks
with the same duration.
Source code in qadence/blocks/analog.py
286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318def __init__(self, blocks: Tuple[AnalogBlock, ...], interaction: Interaction = Interaction.NN): """Stack analog blocks vertically (i.e. in time).
Needed because analog require stricter validation than the general `KronBlock`.
`AnalogKron`s can only be constructed from _**non-global**_, analog blocks with the _**same duration**_. """ if len(blocks) == 0: raise NotImplementedError("Empty KronBlocks not supported")
self.blocks = blocks self.interaction = interaction
qubit_support = QubitSupport() duration = blocks[0].duration for b in blocks: if not isinstance(b, AnalogBlock): raise ValueError("Can only kron `AnalgoBlock`s with other `AnalgoBlock`s.")
if b.qubit_support == QubitSupport("global"): raise ValueError("Blocks with global support cannot be kron'ed.")
if not qubit_support.is_disjoint(b.qubit_support): raise ValueError("Make sure blocks act on distinct qubits!")
if not np.isclose(evaluate(duration), evaluate(b.duration)): raise ValueError("Kron'ed blocks have to have same duration.")
qubit_support += b.qubit_support
self.blocks = blocks
ConstantAnalogRotation(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(alpha=0.0, duration=1000.0, omega=0.0, delta=0.0, phase=0.0), qubit_support=QubitSupport('global'), add_pattern=True)
dataclass
Section titled “
ConstantAnalogRotation(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(alpha=0.0, duration=1000.0, omega=0.0, delta=0.0, phase=0.0), qubit_support=QubitSupport('global'), add_pattern=True)
dataclass
”
Bases: AnalogBlock
Implements a constant analog rotation with interaction dictated by the chosen Hamiltonian.
H/h = ∑ᵢ(Ω/2 cos(φ)*Xᵢ - sin(φ)*Yᵢ - δnᵢ) + Hᵢₙₜ.To construct this block you can use of the following convenience wrappers:
- The general rotation operation AnalogRot
- Shorthands for rotatins around an axis:
AnalogRX,
AnalogRY,
AnalogRZ
WARNING: do not use ConstantAnalogRotation with alpha as differentiable parameter - use
the convenience wrappers mentioned above.
InteractionBlock(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(duration=1000.0), qubit_support=QubitSupport('global'), add_pattern=True)
dataclass
Section titled “
InteractionBlock(tag=None, __array_priority__=1000, _eigenvalues_generator=None, parameters=ParamMap(duration=1000.0), qubit_support=QubitSupport('global'), add_pattern=True)
dataclass
”
Bases: AnalogBlock
Free-evolution for the Hamiltonian interaction term of a register of qubits.
In real interacting quantum devices, it means letting the system evolve freely according to the time-dependent Schrodinger equation. With emulators, this block is translated to an appropriate interaction Hamiltonian, for example, an Ising interaction
Hᵢₙₜ = ∑ᵢⱼ C₆/rᵢⱼ⁶ nᵢnⱼor an XY-interaction
Hᵢₙₜ = ∑ᵢⱼ C₃/rⱼⱼ³ (XᵢXⱼ + ZᵢZⱼ)with nᵢ = (1-Zᵢ)/2.
To construct, use the AnalogInteraction function.
Composite blocks
Section titled “Composite blocks”
chain(*args)
Section titled “
chain(*args)
”Chain blocks sequentially.
On digital backends this can be interpreted loosely as a matrix mutliplication of blocks. In the analog case it chains blocks in time.
| PARAMETER | DESCRIPTION |
|---|---|
*args
|
Blocks to chain. Can also be a generator.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ChainBlock
|
ChainBlock |
Example:
from qadence import X, Y, chain
b = chain(X(0), Y(0))
# or use a generatorb = chain(X(i) for i in range(3))print(b)ChainBlock(0,1,2)├── X(0)├── X(1)└── X(2)Source code in qadence/blocks/utils.py
525354555657585960616263646566676869707172737475767778798081def chain(*args: Union[AbstractBlock, Generator, List[AbstractBlock]]) -> ChainBlock: """Chain blocks sequentially.
On digital backends this can be interpreted loosely as a matrix mutliplication of blocks. In the analog case it chains blocks in time.
Arguments: *args: Blocks to chain. Can also be a generator.
Returns: ChainBlock
Example: ```python exec="on" source="material-block" result="json" from qadence import X, Y, chain
b = chain(X(0), Y(0))
# or use a generator b = chain(X(i) for i in range(3)) print(b) ``` """ # ugly hack to use `AnalogChain` if we are dealing only with analog blocks if len(args) and all( isinstance(a, AnalogBlock) or isinstance(a, AnalogComposite) for a in args ): return analog_chain(*args) # type: ignore[return-value,arg-type] return _construct(ChainBlock, args)
kron(*args)
Section titled “
kron(*args)
”Stack blocks vertically.
On digital backends this can be intepreted loosely as a kronecker product of blocks. In the analog case it executes blocks parallel in time.
| PARAMETER | DESCRIPTION |
|---|---|
*args
|
Blocks to kron. Can also be a generator.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
KronBlock
|
KronBlock |
Example:
from qadence import X, Y, kron
b = kron(X(0), Y(1))
# or use a generatorb = kron(X(i) for i in range(3))print(b)KronBlock(0,1,2)├── X(0)├── X(1)└── X(2)Source code in qadence/blocks/utils.py
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99100101102103104105106107108109110111112113def kron(*args: Union[AbstractBlock, Generator]) -> KronBlock: """Stack blocks vertically.
On digital backends this can be intepreted loosely as a kronecker product of blocks. In the analog case it executes blocks parallel in time.
Arguments: *args: Blocks to kron. Can also be a generator.
Returns: KronBlock
Example: ```python exec="on" source="material-block" result="json" from qadence import X, Y, kron
b = kron(X(0), Y(1))
# or use a generator b = kron(X(i) for i in range(3)) print(b) ``` """ # ugly hack to use `AnalogKron` if we are dealing only with analog blocks if len(args) and all( isinstance(a, AnalogBlock) or isinstance(a, AnalogComposite) for a in args ): return analog_kron(*args) # type: ignore[return-value,arg-type] return _construct(KronBlock, args)
add(*args)
Section titled “
add(*args)
”Sums blocks.
| PARAMETER | DESCRIPTION |
|---|---|
*args
|
Blocks to add. Can also be a generator.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
AddBlock
|
AddBlock |
Example:
from qadence import X, Y, add
b = add(X(0), Y(0))
# or use a generatorb = add(X(i) for i in range(3))print(b)AddBlock(0,1,2)├── X(0)├── X(1)└── X(2)Source code in qadence/blocks/utils.py
116117118119120121122123124125126127128129130131132133134135136def add(*args: Union[AbstractBlock, Generator]) -> AddBlock: """Sums blocks.
Arguments: *args: Blocks to add. Can also be a generator.
Returns: AddBlock
Example: ```python exec="on" source="material-block" result="json" from qadence import X, Y, add
b = add(X(0), Y(0))
# or use a generator b = add(X(i) for i in range(3)) print(b) ``` """ return _construct(AddBlock, args)
AddBlock(blocks)
Section titled “
AddBlock(blocks)
”
Bases: CompositeBlock
Adds blocks.
Constructed via add.
Source code in qadence/blocks/composite.py
259260def __init__(self, blocks: Tuple[AbstractBlock, ...]): self.blocks = blocks
ChainBlock(blocks)
Section titled “
ChainBlock(blocks)
”
Bases: CompositeBlock
Chains blocks sequentially.
Constructed via chain
Source code in qadence/blocks/composite.py
191192def __init__(self, blocks: Tuple[AbstractBlock, ...]): self.blocks = blocks
CompositeBlock(tag=None, __array_priority__=1000)
dataclass
Section titled “
CompositeBlock(tag=None, __array_priority__=1000)
dataclass
”
Bases: AbstractBlock
Block which composes multiple blocks into one larger block (which can again be composed).
Composite blocks are constructed via chain,
kron, and add.
KronBlock(blocks)
Section titled “
KronBlock(blocks)
”
Bases: CompositeBlock
Stacks blocks horizontally.
Constructed via kron.
Source code in qadence/blocks/composite.py
221222223224225226227228229230231232233234235def __init__(self, blocks: Tuple[AbstractBlock, ...]): if len(blocks) == 0: raise NotImplementedError("Empty KronBlocks not supported")
qubit_support = QubitSupport() for b in blocks: assert ( QubitSupportType.GLOBAL, ) != b.qubit_support, "Blocks with global support cannot be kron'ed." assert qubit_support.is_disjoint( b.qubit_support ), "Make sure blocks act on distinct qubits!" qubit_support += b.qubit_support
self.blocks = blocksConverting blocks to matrices
Section titled “Converting blocks to matrices”
block_to_tensor(block, values={}, qubit_support=None, use_full_support=False, tensor_type=TensorType.DENSE, endianness=Endianness.BIG, device=None)
Section titled “
block_to_tensor(block, values={}, qubit_support=None, use_full_support=False, tensor_type=TensorType.DENSE, endianness=Endianness.BIG, device=None)
”Convert a block into a torch tensor.
| PARAMETER | DESCRIPTION |
|---|---|
block
|
The block to convert.
TYPE:
|
values
|
A optional dict with values for parameters.
TYPE:
|
qubit_support
|
The qubit_support of the block.
TYPE:
|
use_full_support
|
True infers the total number of qubits.
TYPE:
|
tensor_type
|
the target tensor type.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Tensor
|
A torch.Tensor. |
Examples:
from qadence import hea, hamiltonian_factory, Z, block_to_tensor
block = hea(2,2)print(block_to_tensor(block))
# In case you have a diagonal observable, you can useobs = hamiltonian_factory(2, detuning = Z)print(block_to_tensor(obs, tensor_type="SparseDiagonal"))tensor([[[ 0.3554+0.3119j, -0.2412-0.5711j, -0.3515+0.0019j, -0.4690+0.2204j], [-0.0097-0.4932j, 0.2821+0.3119j, -0.6343+0.0848j, -0.4093-0.0528j], [-0.0809-0.5268j, -0.2912-0.4593j, 0.2103+0.2089j, -0.1240-0.5630j], [-0.3588-0.3468j, -0.0729-0.3711j, -0.2612-0.5576j, 0.3390+0.3376j]]], grad_fn=<UnsafeViewBackward0>)tensor(indices=tensor([[0, 3], [0, 3]]), values=tensor([ 2.+0.j, -2.+0.j]), size=(4, 4), nnz=2, layout=torch.sparse_coo)Source code in qadence/blocks/block_to_tensor.py
309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361def block_to_tensor( block: AbstractBlock, values: dict[str, TNumber | torch.Tensor] = {}, qubit_support: tuple | None = None, use_full_support: bool = False, tensor_type: TensorType = TensorType.DENSE, endianness: Endianness = Endianness.BIG, device: torch.device = None,) -> torch.Tensor: """ Convert a block into a torch tensor.
Arguments: block (AbstractBlock): The block to convert. values (dict): A optional dict with values for parameters. qubit_support (tuple): The qubit_support of the block. use_full_support (bool): True infers the total number of qubits. tensor_type (TensorType): the target tensor type.
Returns: A torch.Tensor.
Examples: ```python exec="on" source="material-block" result="json" from qadence import hea, hamiltonian_factory, Z, block_to_tensor
block = hea(2,2) print(block_to_tensor(block))
# In case you have a diagonal observable, you can use obs = hamiltonian_factory(2, detuning = Z) print(block_to_tensor(obs, tensor_type="SparseDiagonal")) ``` """ from qadence.blocks import embedding
(ps, embed) = embedding(block) values = embed(ps, values) if tensor_type == TensorType.DENSE: return _block_to_tensor_embedded( block, values, qubit_support, use_full_support, endianness=endianness, device=device, )
elif tensor_type == TensorType.SPARSEDIAGONAL: t = block_to_diagonal(block, values, endianness=endianness) indices, values, size = torch.nonzero(t), t[t != 0], len(t) indices = torch.stack((indices.flatten(), indices.flatten())) return torch.sparse_coo_tensor(indices, values, (size, size))